[CalendarServer-changes] [3036] CalendarServer/trunk/twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Tue Sep 23 19:45:19 PDT 2008
Revision: 3036
http://trac.macosforge.org/projects/calendarserver/changeset/3036
Author: cdaboo at apple.com
Date: 2008-09-23 19:45:12 -0700 (Tue, 23 Sep 2008)
Log Message:
-----------
Provide indication to client about what changed when an invite was auto-processed.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/customxml.py
CalendarServer/trunk/twistedcaldav/ical.py
CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
CalendarServer/trunk/twistedcaldav/scheduling/itip.py
CalendarServer/trunk/twistedcaldav/scheduling/processing.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py 2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/customxml.py 2008-09-24 02:45:12 UTC (rev 3036)
@@ -28,6 +28,11 @@
from twistedcaldav.ical import Component as iComponent
+from vobject.icalendar import utc
+from vobject.icalendar import dateTimeToString
+
+import datetime
+
calendarserver_namespace = "http://calendarserver.org/ns/"
calendarserver_proxy_compliance = (
@@ -330,7 +335,202 @@
namespace = calendarserver_namespace
name = "free-busy-url"
+class ScheduleChanges (davxml.WebDAVElement):
+ """
+ Change indicator for a scheduling message.
+ """
+ namespace = calendarserver_namespace
+ name = "schedule-changes"
+ protected = True
+ hidden = True
+ allowed_children = {
+ (calendarserver_namespace, "dtstamp" ) : (0, 1), # Have to allow 0 as element is empty in PROPFIND requests
+ (calendarserver_namespace, "action" ) : (0, 1), # Have to allow 0 as element is empty in PROPFIND requests
+ }
+class DTStamp (davxml.WebDAVTextElement):
+ """
+ A UTC timestamp in iCal format.
+ """
+ namespace = calendarserver_namespace
+ name = "dtstamp"
+
+ def __init__(self, *children):
+ super(DTStamp, self).__init__(children)
+ self.children = (davxml.PCDATAElement(dateTimeToString(datetime.datetime.now(tz=utc))),)
+
+class Action (davxml.WebDAVElement):
+ """
+ A UTC timestamp in iCal format.
+ """
+ namespace = calendarserver_namespace
+ name = "action"
+ allowed_children = {
+ (calendarserver_namespace, "create" ) : (0, 1),
+ (calendarserver_namespace, "update" ) : (0, 1),
+ (calendarserver_namespace, "cancel" ) : (0, 1),
+ (calendarserver_namespace, "reply" ) : (0, 1),
+ }
+
+class Create (davxml.WebDAVEmptyElement):
+ """
+ Event created.
+ """
+ namespace = calendarserver_namespace
+ name = "create"
+
+class Update (davxml.WebDAVElement):
+ """
+ Event updated.
+ """
+ namespace = calendarserver_namespace
+ name = "update"
+ allowed_children = {
+ (calendarserver_namespace, "changes" ) : (1, 1),
+ (calendarserver_namespace, "recurrences" ) : (0, 1),
+ }
+
+class Cancel (davxml.WebDAVElement):
+ """
+ Event cancelled.
+ """
+ namespace = calendarserver_namespace
+ name = "cancel"
+ allowed_children = {
+ (calendarserver_namespace, "recurrences" ) : (0, 1),
+ }
+
+class Reply (davxml.WebDAVElement):
+ """
+ Event replied to.
+ """
+ namespace = calendarserver_namespace
+ name = "reply"
+ allowed_children = {
+ (calendarserver_namespace, "attendee" ) : (1, 1),
+ (calendarserver_namespace, "partstat" ) : (0, 1),
+ (calendarserver_namespace, "private-comment" ) : (0, 1),
+ }
+
+class Attendee (davxml.WebDAVTextElement):
+ """
+ An attendee calendar user address.
+ """
+ namespace = calendarserver_namespace
+ name = "attendee"
+
+class PartStat (davxml.WebDAVEmptyElement):
+ """
+ An attendee partstat.
+ """
+ namespace = calendarserver_namespace
+ name = "partstat"
+
+class PrivateComment (davxml.WebDAVEmptyElement):
+ """
+ An attendee private comment.
+ """
+ namespace = calendarserver_namespace
+ name = "private-comment"
+
+class Changes (davxml.WebDAVElement):
+ """
+ Changes to an event.
+ """
+ namespace = calendarserver_namespace
+ name = "changes"
+ allowed_children = {
+ (calendarserver_namespace, "datetime" ) : (0, 1),
+ (calendarserver_namespace, "location" ) : (0, 1),
+ (calendarserver_namespace, "summary" ) : (0, 1),
+ (calendarserver_namespace, "description" ) : (0, 1),
+ (calendarserver_namespace, "recurrence" ) : (0, 1),
+ (calendarserver_namespace, "status" ) : (0, 1),
+ (calendarserver_namespace, "attendees" ) : (0, 1),
+ (calendarserver_namespace, "attendee-partstat" ) : (0, 1),
+ }
+
+class Datetime (davxml.WebDAVEmptyElement):
+ """
+ Date time change.
+ """
+ namespace = calendarserver_namespace
+ name = "datetime"
+
+class Location (davxml.WebDAVEmptyElement):
+ """
+ Location changed.
+ """
+ namespace = calendarserver_namespace
+ name = "location"
+
+class Summary (davxml.WebDAVEmptyElement):
+ """
+ Summary changed.
+ """
+ namespace = calendarserver_namespace
+ name = "summary"
+
+class Description (davxml.WebDAVEmptyElement):
+ """
+ Description changed.
+ """
+ namespace = calendarserver_namespace
+ name = "description"
+
+class Recurrence (davxml.WebDAVEmptyElement):
+ """
+ Recurrence changed.
+ """
+ namespace = calendarserver_namespace
+ name = "recurrence"
+
+class Status (davxml.WebDAVEmptyElement):
+ """
+ Status changed.
+ """
+ namespace = calendarserver_namespace
+ name = "status"
+
+class Attendees (davxml.WebDAVEmptyElement):
+ """
+ Attendees changed.
+ """
+ namespace = calendarserver_namespace
+ name = "attendees"
+
+class AttendeePartStat (davxml.WebDAVEmptyElement):
+ """
+ Attendee partstats changed.
+ """
+ namespace = calendarserver_namespace
+ name = "attendee-partstat"
+
+class Recurrences (davxml.WebDAVElement):
+ """
+ Changes to an event.
+ """
+ namespace = calendarserver_namespace
+ name = "recurrences"
+ allowed_children = {
+ (calendarserver_namespace, "master" ) : (0, 1),
+ (calendarserver_namespace, "recurrenceid" ) : (0, None),
+ }
+
+class Master (davxml.WebDAVEmptyElement):
+ """
+ Master instance changed.
+ """
+ namespace = calendarserver_namespace
+ name = "master"
+
+class RecurrenceID (davxml.WebDAVTextElement):
+ """
+ A recurrence instance changed.
+ """
+ namespace = calendarserver_namespace
+ name = "recurrenceid"
+
##
# Extensions to davxml.ResourceType
##
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2008-09-24 02:45:12 UTC (rev 3036)
@@ -834,6 +834,22 @@
rid = self.getRecurrenceIDUTC()
return (rid,)
+ def isRecurring(self):
+ """
+ Check whether any recurrence properties are present in any component.
+ """
+
+ # Extract appropriate sub-component if this is a VCALENDAR
+ if self.name() == "VCALENDAR":
+ for component in self.subcomponents():
+ if component.name() != "VTIMEZONE" and component.isRecurring():
+ return True
+ else:
+ for propname in ("RRULE", "RDATE", "EXDATE", "RECUURENCE-ID",):
+ if self.hasProperty(propname):
+ return True
+ return False
+
def deriveInstance(self, rid):
"""
Derive an instance from the master component that has the provided RECURRENCE-ID, but
Modified: CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py 2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py 2008-09-24 02:45:12 UTC (rev 3036)
@@ -152,7 +152,7 @@
if not recipient.principal.autoSchedule():
try:
processor = ImplicitProcessor()
- processed, autoprocessed = (yield processor.doImplicitProcessing(
+ processed, autoprocessed, changes = (yield processor.doImplicitProcessing(
self.scheduler.request,
self.scheduler.calendar,
self.scheduler.originator,
@@ -165,6 +165,7 @@
returnValue(False)
else:
processed = autoprocessed = False
+ changes = None
if autoprocessed:
# No need to write the inbox item as it has already been auto-processed
@@ -201,6 +202,10 @@
# Store CALDAV:schedule-state property
child.writeDeadProperty(caldavxml.ScheduleState(caldavxml.ScheduleProcessed() if processed else caldavxml.ScheduleUnprocessed()))
+ # Store CS:schedule-changes property if present
+ if changes:
+ child.writeDeadProperty(changes)
+
# Look for auto-schedule option
if recipient.principal.autoSchedule():
autoresponses.append((recipient.principal, recipient.inbox, child))
Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2008-09-24 02:45:12 UTC (rev 3036)
@@ -18,6 +18,8 @@
from twistedcaldav.log import Logger
from twistedcaldav.scheduling.itip import iTipGenerator
+from vobject.icalendar import dateTimeToString
+
"""
Class that handles diff'ing two calendar objects.
"""
@@ -113,6 +115,58 @@
# component instance from the old one
return self._compareComponents()
+ def whatIsDifferent(self):
+ """
+ Compare the two calendar objects in their entirety and return a list of properties
+ and PARTSTAT parameters that are different.
+ """
+
+ # First get uid/rid map of components
+ def mapComponents(calendar):
+ map = {}
+ for component in calendar.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ name = component.name()
+ uid = component.propertyValue("UID")
+ rid = component.getRecurrenceIDUTC()
+ map[(name, uid, rid,)] = component
+ return map
+
+ props_changed = set()
+ rids = set()
+
+ map1 = mapComponents(self.calendar1)
+ set1 = set(map1.keys())
+ map2 = mapComponents(self.calendar2)
+ set2 = set(map2.keys())
+
+ # Now verify that each component in set1 matches what is in set2
+ for key in (set1 & set2):
+ component1 = map1[key]
+ component2 = map2[key]
+ self._diffComponents(component1, component2, props_changed, rids)
+
+ # Now verify that each additional component in set1 matches a derived component in set2
+ for key in set1 - set2:
+ component1 = map1[key]
+ component2 = self.calendar2.deriveInstance(key[2])
+ if component2 is None:
+ continue
+ self._diffComponents(component1, component2, props_changed, rids)
+
+ # Now verify that each additional component in set1 matches a derived component in set2
+ for key in set2 - set1:
+ component1 = self.calendar1.deriveInstance(key[2])
+ if component1 is None:
+ continue
+ component2 = map2[key]
+ self._diffComponents(component1, component2, props_changed, rids)
+
+ if not self.calendar1.isRecurring() and not self.calendar2.isRecurring() or not props_changed:
+ rids = None
+ return props_changed, rids
+
def _checkVCALENDARProperties(self):
# Get property differences in the VCALENDAR objects
@@ -243,3 +297,41 @@
return False, False
return True, len(propdiff) == 0
+
+ def _diffComponents(self, comp1, comp2, changed, rids):
+
+ assert isinstance(comp1, Component) and isinstance(comp2, Component)
+
+ if comp1.name() != comp2.name():
+ log.debug("Component names are different: '%s' and '%s'" % (comp1.name(), comp2.name()))
+ return
+
+ # Diff all the properties
+ comp1.transformAllFromNative()
+ comp2.transformAllFromNative()
+ propdiff = set(comp1.properties()) ^ set(comp2.properties())
+ comp1.transformAllToNative()
+ comp2.transformAllToNative()
+
+ regular_changes = [prop.name() for prop in propdiff if prop.name() != "ATTENDEE"]
+ changed.update(regular_changes)
+
+ attendees = set([prop for prop in propdiff if prop.name() == "ATTENDEE"])
+ done_attendee = ("ATTENDEE" in changed)
+ done_partstat = ("PARTSTAT" in changed)
+ for ctr, attendee in enumerate(attendees):
+ for check_ctr, check_attendee in enumerate(attendees):
+ if (ctr != check_ctr) and check_attendee.value() == attendee.value():
+ if check_attendee.params().get("PARTSTAT", ("NEEDS-ACTION",)) != attendee.params().get("PARTSTAT", ("NEEDS-ACTION",)):
+ changed.add("PARTSTAT")
+ done_partstat = True
+ break
+ else:
+ changed.add("ATTENDEE")
+ done_attendee = True
+ if done_attendee and done_partstat:
+ break
+
+ if regular_changes or done_attendee or done_partstat:
+ rid = comp1.getRecurrenceIDUTC()
+ rids.add(dateTimeToString(rid) if rid is not None else "")
Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-09-24 02:45:12 UTC (rev 3036)
@@ -82,11 +82,16 @@
@param calendar: the calendar object to apply the REQUEST to
@type calendar:
- @return: calendar object ready to save, or C{None} (request should be ignored)
+ @return: a C{tuple} of:
+ calendar object ready to save, or C{None} (request should be ignored)
+ a C{set} of iCalendar properties that changed, or C{None},
+ a C{set} of recurrences that changed, or C{None}
"""
# Merge Organizer data with Attendee's own changes (VALARMs only for now).
-
+ from twistedcaldav.scheduling.icaldiff import iCalDiff
+ props_changed, rids = iCalDiff(calendar, itip_message).whatIsDifferent()
+
# Different behavior depending on whether a master component is present or not
current_master = calendar.masterComponent()
if current_master:
@@ -110,7 +115,7 @@
iTipProcessing.transferAlarms(calendar, master_valarms, component)
# Replace the entire object
- return new_calendar
+ return new_calendar, props_changed, rids
else:
# Need existing tzids
@@ -129,7 +134,7 @@
iTipProcessing.fixForiCal3((component,), recipient)
# Write back the modified object
- return calendar
+ return calendar, props_changed, rids
@staticmethod
def processCancel(itip_message, calendar):
@@ -146,6 +151,7 @@
@return: C{tuple} of:
C{bool} : C{True} if processed, C{False} if scheduling message should be ignored
C{bool} : C{True} if calendar object should be deleted, C{False} otherwise
+ C{set} : set of Recurrence-IDs for cancelled instances, or C{None} if all cancelled
"""
assert itip_message.propertyValue("METHOD") == "CANCEL", "iTIP message must have METHOD:CANCEL"
@@ -153,7 +159,7 @@
# Check to see if this is a cancel of the entire event
if itip_message.masterComponent() is not None:
- return True, True
+ return True, True, None
# iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
# So we need to iterate over each iTIP component.
@@ -161,6 +167,7 @@
# Get the existing calendar master object if it exists
calendar_master = calendar.masterComponent()
exdates = []
+ rids = set()
# Look at each component in the iTIP message
for component in itip_message.subcomponents():
@@ -169,6 +176,7 @@
# Extract RECURRENCE-ID value from component
rid = component.getRecurrenceIDUTC()
+ rids.add(rid)
# Get the one that matches in the calendar
overridden = calendar.overriddenComponent(rid)
@@ -195,9 +203,9 @@
# in which case the calendar object is empty (except for VTIMEZONEs).
if calendar.mainType() is None:
# Delete the now empty calendar object
- return True, True
+ return True, True, None
else:
- return True, False
+ return True, False, rids
@staticmethod
def processReply(itip_message, calendar):
@@ -211,7 +219,9 @@
@param calendar: the calendar object to apply the REPLY to
@type calendar:
- @return: C{True} if processed, C{False} if scheduling message should be ignored
+ @return: a C{tuple} of:
+ C{True} if processed, C{False} if scheduling message should be ignored
+ C{tuple} of change info
"""
assert itip_message.propertyValue("METHOD") == "REPLY", "iTIP message must have METHOD:REPLY"
@@ -225,8 +235,16 @@
old_master = calendar.masterComponent()
new_master = itip_message.masterComponent()
attendees = set()
+ partstat_changed = False
+ private_comment_changed = False
+ rids = set() if old_master.isRecurring() else None
if new_master:
- attendees.add(iTipProcessing.updateAttendeeData(new_master, old_master))
+ attendee, partstat, private_comment = iTipProcessing.updateAttendeeData(new_master, old_master)
+ attendees.add(attendee)
+ partstat_changed = partstat_changed or partstat
+ private_comment_changed = private_comment_changed or private_comment
+ if rids is not None:
+ rids.add("")
# Now do all overridden ones
for itip_component in itip_message.subcomponents():
@@ -246,10 +264,15 @@
match_component = calendar.deriveInstance(rid)
calendar.addComponent(match_component)
- attendees.add(iTipProcessing.updateAttendeeData(itip_component, match_component))
-
- return True, attendees
+ attendee, partstat, private_comment = iTipProcessing.updateAttendeeData(itip_component, match_component)
+ attendees.add(attendee)
+ partstat_changed = partstat_changed or partstat
+ private_comment_changed = private_comment_changed or private_comment
+ if rids is not None:
+ rids.add(rid)
+ return True, (attendees, partstat_changed, private_comment_changed, rids)
+
@staticmethod
def updateAttendeeData(from_component, to_component):
"""
@@ -262,6 +285,10 @@
@type to_component:
"""
+ # Track what changed
+ partstat_changed = False
+ private_comment_changed = False
+
# Get attendee in from_component - there MUST be only one
attendees = tuple(from_component.properties("ATTENDEE"))
assert len(attendees) == 1, "There must be one and only one ATTENDEE property in a REPLY"
@@ -271,7 +298,9 @@
# Now find matching ATTENDEE in to_component
existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
if existing_attendee:
+ oldpartstat = existing_attendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0]
existing_attendee.params().setdefault("PARTSTAT", [partstat])[0] = partstat
+ partstat_changed = (oldpartstat != partstat)
# Handle attendee comments
@@ -307,6 +336,8 @@
# Set value empty
private_comment.setValue("")
+ private_comment_changed = True
+
elif attendee_comment is not None and private_comment is None:
# Add new property
@@ -319,6 +350,8 @@
}
)
to_component.addProperty(private_comment)
+
+ private_comment_changed = True
else:
# Remove all property parameters
@@ -329,10 +362,13 @@
private_comment.params()["X-CALENDARSERVER-DTSTAMP"] = [dateTimeToString(datetime.datetime.now(tz=utc))]
# Set new value
+ oldvalue = private_comment.value()
private_comment.setValue(attendee_comment.value())
- return attendee.value()
+ private_comment_changed = (oldvalue != attendee_comment.value())
+ return attendee.value(), partstat_changed, private_comment_changed
+
@staticmethod
def transferAlarms(from_calendar, master_valarms, to_component, remove_matched=False):
Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2008-09-24 02:45:12 UTC (rev 3036)
@@ -22,6 +22,7 @@
from hashlib import md5
from twisted.web2.dav.util import joinURL
from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav import customxml
import time
__all__ = [
@@ -132,7 +133,7 @@
yield self.getRecipientsCopy()
if self.recipient_calendar is None:
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- returnValue((True, True,))
+ returnValue((True, True, None,))
# Handle new items differently than existing ones.
if self.method == "REPLY":
@@ -140,7 +141,7 @@
elif self.method == "REFRESH":
# With implicit we ignore refreshes.
# TODO: for iMIP etc we do need to handle them
- result = (True, True,)
+ result = (True, True, None,)
returnValue(result)
@@ -148,19 +149,41 @@
def doImplicitOrganizerUpdate(self):
# Check to see if this is a valid reply
- result, processed_attendees = iTipProcessing.processReply(self.message, self.recipient_calendar)
+ result, processed = iTipProcessing.processReply(self.message, self.recipient_calendar)
if result:
# Update the attendee's copy of the event
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REPLY, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
recipient_calendar_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar))
- result = (True, False,)
+ # Build the schedule-changes XML element
+ processed_attendees, partstat_changed, private_comment_changed, rids = processed
+ reply_details = (customxml.Attendee.fromString(tuple(processed_attendees)[0]),)
+ if partstat_changed:
+ reply_details += (customxml.PartStat(),)
+ if private_comment_changed:
+ reply_details += (customxml.PrivateComment(),)
+ if rids is not None:
+ recurrences = []
+ if "" in rids:
+ recurrences.append(customxml.Master())
+ recurrences.extend([customxml.RecurrenceID.fromString(rid) for rid in rids if rid != ""])
+ reply_details += (customxml.Recurrences(*recurrences),)
+
+ changes = customxml.ScheduleChanges(
+ customxml.DTStamp(),
+ customxml.Action(
+ customxml.Reply(*reply_details),
+ ),
+ )
+
self.updateAllAttendeesExceptSome(recipient_calendar_resource, processed_attendees)
+ result = (True, False, changes,)
+
else:
# Ignore scheduling message
- result = (True, True,)
+ result = (True, True, None,)
returnValue(result)
@@ -186,7 +209,7 @@
# Handle new items differently than existing ones.
if self.new_resource and self.method == "CANCEL":
- result = (True, True,)
+ result = (True, True, None)
else:
result = (yield self.doImplicitAttendeeUpdate())
@@ -197,17 +220,17 @@
# Different based on method
if self.method == "REQUEST":
- result = (yield self.doImplicitAttendeRequest())
+ result = (yield self.doImplicitAttendeeRequest())
elif self.method == "CANCEL":
- result = (yield self.doImplicitAttendeCancel())
+ result = (yield self.doImplicitAttendeeCancel())
elif self.method == "ADD":
# TODO: implement ADD
- result = (False, False,)
+ result = (False, False, None)
returnValue(result)
@inlineCallbacks
- def doImplicitAttendeRequest(self):
+ def doImplicitAttendeeRequest(self):
# If there is no existing copy, then look for default calendar and copy it here
if self.new_resource:
@@ -225,55 +248,116 @@
new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr)
name = md5(str(new_calendar) + str(time.time()) + default.fp.path).hexdigest() + ".ics"
yield self.writeCalendarResource(defaultURL, default, name, new_calendar)
- result = (True, False,)
+
+ # Build the schedule-changes XML element
+ changes = customxml.ScheduleChanges(
+ customxml.DTStamp(),
+ customxml.Action(
+ customxml.Create(),
+ ),
+ )
+ result = (True, False, changes,)
else:
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:REQUEST, UID: '%s' - new not processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- result = (False, False,)
+ result = (False, False, None,)
else:
# Processing update to existing event
- new_calendar = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
+ new_calendar, props_changed, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
if new_calendar:
# Update the attendee's copy of the event
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, new_calendar)
- result = (True, False,)
+
+ # Build the schedule-changes XML element
+ changes = ()
+ if props_changed:
+ changemap = {
+ "DTSTART" : customxml.Datetime(),
+ "DTEND" : customxml.Datetime(),
+ "DURATION" : customxml.Datetime(),
+ "DUE" : customxml.Datetime(),
+ "COMPLETED" : customxml.Datetime(),
+ "LOCATION" : customxml.Location(),
+ "SUMMARY" : customxml.Summary(),
+ "DESCRIPTION" : customxml.Description(),
+ "RRULE" : customxml.Recurrence(),
+ "RDATE" : customxml.Recurrence(),
+ "EXDATE" : customxml.Recurrence(),
+ "STATUS" : customxml.Status(),
+ "ATTENDEE" : customxml.Attendees(),
+ "PARTSTAT" : customxml.PartStat(),
+ }
+ changes += tuple([changemap[prop] for prop in props_changed if prop in changemap])
+ update_details = (customxml.Changes(*changes),)
+ if rids is not None:
+ recurrences = []
+ if "" in rids:
+ recurrences.append(customxml.Master())
+ recurrences.extend([customxml.RecurrenceID.fromString(rid) for rid in rids if rid != ""])
+ update_details += (customxml.Recurrences(*recurrences),)
+ changes = customxml.ScheduleChanges(
+ customxml.DTStamp(),
+ customxml.Action(
+ customxml.Update(*update_details),
+ ),
+ )
+ result = (True, False, changes,)
else:
# Request needs to be ignored
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- result = (True, True,)
+ result = (True, True, None,)
returnValue(result)
@inlineCallbacks
- def doImplicitAttendeCancel(self):
+ def doImplicitAttendeeCancel(self):
# If there is no existing copy, then ignore
if self.recipient_calendar is None:
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- result = (True, True,)
+ result = (True, True, None)
else:
# Check to see if this is a cancel of the entire event
- processed_message, delete_original = iTipProcessing.processCancel(self.message, self.recipient_calendar)
+ processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar)
if processed_message:
if delete_original:
# Delete the attendee's copy of the event
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
yield self.deleteCalendarResource(self.recipient_calendar_collection, self.recipient_calendar_name)
- result = (True, False,)
+
+ # Build the schedule-changes XML element
+ changes = customxml.ScheduleChanges(
+ customxml.DTStamp(),
+ customxml.Action(
+ customxml.Cancel(),
+ ),
+ )
+ result = (True, False, changes,)
else:
# Update the attendee's copy of the event
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
- result = (True, False,)
+
+ # Build the schedule-changes XML element
+ changes = customxml.ScheduleChanges(
+ customxml.DTStamp(),
+ customxml.Action(
+ customxml.Cancel(),
+ customxml.Recurrences(
+ *[customxml.RecurrenceID.fromString(rid) for rid in rids]
+ ),
+ ),
+ )
+ result = (True, False, changes)
else:
log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- result = (True, True,)
+ result = (True, True, None)
returnValue(result)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py 2008-09-23 20:59:01 UTC (rev 3035)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py 2008-09-24 02:45:12 UTC (rev 3036)
@@ -17,6 +17,7 @@
from twistedcaldav.ical import Component
import twistedcaldav.test.util
+import itertools
class ICalDiff (twistedcaldav.test.util.TestCase):
"""
@@ -1163,3 +1164,910 @@
for description, calendar1, calendar2, attendee, result in data:
differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
self.assertEqual(differ.attendeeMerge(attendee), result, msg=description)
+
+ def test_what_is_different(self):
+
+ data1 = (
+ (
+ "#1.1 Simple component, no change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (),
+ (),
+ ),
+ (
+ "#1.2 Simple component, one property change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test1
+END:VEVENT
+END:VCALENDAR
+""",
+ ("SUMMARY",),
+ (),
+ ),
+ (
+ "#1.3 Simple component, one property change, one addition, one removal",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+LOCATION:The Office
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test1
+DESCRIPTION:Something to do.
+END:VEVENT
+END:VCALENDAR
+""",
+ ("SUMMARY", "LOCATION", "DESCRIPTION",),
+ (),
+ ),
+ (
+ "#1.4 Simple component, add attendee",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ("ATTENDEE",),
+ (),
+ ),
+ (
+ "#1.5 Simple component, remove attendee",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ("ATTENDEE",),
+ (),
+ ),
+ (
+ "#1.6 Simple component, attendee PARTSTAT only",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ("PARTSTAT",),
+ (),
+ ),
+ (
+ "#1.7 Simple component, attendee PARTSTAT and addition",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ("PARTSTAT", "ATTENDEE",),
+ (),
+ ),
+ (
+ "#1.8 Simple component, attendee RSVP only",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (),
+ (),
+ ),
+ )
+
+ data2 = (
+ (
+ "#2.1 Simple recurring component, no change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ (),
+ (),
+ ),
+ (
+ "#2.2 Simple component, one property change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test1
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ("SUMMARY",),
+ ("",),
+ ),
+ (
+ "#2.3 Simple component, one property change, one addition, one removal",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+LOCATION:The Office
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test1
+DESCRIPTION:Something to do.
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ("SUMMARY", "LOCATION", "DESCRIPTION",),
+ ("",),
+ ),
+ (
+ "#2.4 Simple component, add attendee",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ("ATTENDEE",),
+ ("",),
+ ),
+ (
+ "#2.5 Simple component, remove attendee",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ("ATTENDEE",),
+ ("",),
+ ),
+ (
+ "#2.6 Simple component, attendee PARTSTAT only",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ("PARTSTAT",),
+ ("",),
+ ),
+ (
+ "#2.7 Simple component, attendee PARTSTAT and addition",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT="ACCEPTED":mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ("PARTSTAT", "ATTENDEE",),
+ ("",),
+ ),
+ )
+
+ data3 = (
+ (
+ "#3.1 Complex recurring component, no change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (),
+ (),
+ ),
+ (
+ "#3.2 Simple component, one property change in instance",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test1
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ("SUMMARY",),
+ ("20080602T120000Z",),
+ ),
+ (
+ "#3.3 Simple component, one property change in master",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test1
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ("SUMMARY",),
+ ("",),
+ ),
+ (
+ "#3.4 Simple component, one property change in master and instance",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test1
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test2
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ("SUMMARY",),
+ ("", "20080602T120000Z",),
+ ),
+ (
+ "#3.5 Simple component, different property change in master and instance",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test1
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+Description:Instance
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ("SUMMARY", "DESCRIPTION"),
+ ("", "20080602T120000Z",),
+ ),
+ (
+ "#3.6 Simple component, instance added no change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ (),
+ (),
+ ),
+ (
+ "#3.7 Simple component, instance added time change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ ("DTSTART", "DTEND", ),
+ ("20080602T120000Z",),
+ ),
+ (
+ "#3.8 Simple component, instance removed no change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ (),
+ (),
+ ),
+ (
+ "#3.9 Simple component, instance removed time change",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+SUMMARY:Test
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ ("DTSTART", "DTEND", ),
+ ("20080602T120000Z",),
+ ),
+ )
+
+ for description, calendar1, calendar2, changes, rids in itertools.chain(data1, data2, data3,):
+ differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+ expected_changes = set(changes)
+ expected_rids = set(rids) if rids else None
+ got_changes, got_rids = differ.whatIsDifferent()
+ self.assertEqual(got_changes, expected_changes, msg="%s expected changes: '%s', got: '%s'" % (description, expected_changes, got_changes,))
+ self.assertEqual(got_rids, expected_rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, expected_rids, got_rids,))
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080923/e5c0eee8/attachment-0001.html
More information about the calendarserver-changes
mailing list