[CalendarServer-changes] [3672] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Sun Feb 15 17:17:42 PST 2009
Revision: 3672
http://trac.macosforge.org/projects/calendarserver/changeset/3672
Author: cdaboo at apple.com
Date: 2009-02-15 17:17:37 -0800 (Sun, 15 Feb 2009)
Log Message:
-----------
Use STATUS:CANCELLED for attendee events that get cancelled.
Modified Paths:
--------------
CalendarServer/trunk/run
CalendarServer/trunk/twistedcaldav/ical.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
Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run 2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/run 2009-02-16 01:17:37 UTC (rev 3672)
@@ -692,7 +692,7 @@
caldavtester="${top}/CalDAVTester";
-svn_get "CalDAVTester" "${caldavtester}" "${svn_uri_base}/CalDAVTester/trunk" 3653;
+svn_get "CalDAVTester" "${caldavtester}" "${svn_uri_base}/CalDAVTester/trunk" 3671;
#
# PyFlakes
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2009-02-16 01:17:37 UTC (rev 3672)
@@ -1013,11 +1013,18 @@
"""
@raise ValueError: if the given calendar data is not valid.
"""
- if self.name() != "VCALENDAR": raise ValueError("Not a calendar")
- if not self.resourceType(): raise ValueError("Unknown resource type")
+ if self.name() != "VCALENDAR":
+ log.debug("Not a calendar: %s" % (self,))
+ raise ValueError("Not a calendar")
+ if not self.resourceType():
+ log.debug("Unknown resource type: %s" % (self,))
+ raise ValueError("Unknown resource type")
version = self.propertyValue("VERSION")
- if version != "2.0": raise ValueError("Not a version 2.0 iCalendar (version=%s)" % (version,))
+ if version != "2.0":
+ msg = "Not a version 2.0 iCalendar (version=%s)" % (version,)
+ log.debug(msg)
+ raise ValueError(msg)
def validateForCalDAV(self):
"""
@@ -1028,7 +1035,9 @@
# Disallowed in CalDAV-Access-08, section 4.1
if self.hasProperty("METHOD"):
- raise ValueError("METHOD property is not allowed in CalDAV iCalendar data")
+ msg = "METHOD property is not allowed in CalDAV iCalendar data"
+ log.debug(msg)
+ raise ValueError(msg)
self.validateComponentsForCalDAV(False)
@@ -1054,7 +1063,9 @@
for subcomponent in self.subcomponents():
# Disallowed in CalDAV-Access-08, section 4.1
if not method and subcomponent.hasProperty("METHOD"):
- raise ValueError("METHOD property is not allowed in CalDAV iCalendar data")
+ msg = "METHOD property is not allowed in CalDAV iCalendar data"
+ log.debug(msg)
+ raise ValueError(msg)
if subcomponent.name() == "VTIMEZONE":
timezones.add(subcomponent.propertyValue("TZID"))
@@ -1063,29 +1074,36 @@
ctype = subcomponent.name()
else:
if ctype != subcomponent.name():
- raise ValueError("Calendar resources may not contain more than one type of calendar " +
- "component (%s and %s found)" % (ctype, subcomponent.name()))
+ msg = "Calendar resources may not contain more than one type of calendar component (%s and %s found)" % (ctype, subcomponent.name())
+ log.debug(msg)
+ raise ValueError(msg)
if ctype not in allowedComponents:
- raise ValueError("Component type: %s not allowed" % (ctype,))
+ msg = "Component type: %s not allowed" % (ctype,)
+ log.debug(msg)
+ raise ValueError(msg)
uid = subcomponent.propertyValue("UID")
if uid is None:
- raise ValueError("All components must have UIDs")
+ msg = "All components must have UIDs"
+ log.debug(msg)
+ raise ValueError(msg)
rid = subcomponent.getRecurrenceIDUTC()
# Verify that UIDs are the same
if component_id is None:
component_id = uid
elif component_id != uid:
- raise ValueError("Calendar resources may not contain components with different UIDs " +
- "(%s and %s found)" % (component_id, subcomponent.propertyValue("UID")))
+ msg = "Calendar resources may not contain components with different UIDs (%s and %s found)" % (component_id, subcomponent.propertyValue("UID"))
+ log.debug(msg)
+ raise ValueError(msg)
# Verify that there is only one master component
if rid is None:
if got_master:
- raise ValueError("Calendar resources may not contain components with the same UIDs and no Recurrence-IDs " +
- "(%s and %s found)" % (component_id, subcomponent.propertyValue("UID")))
+ msg = "Calendar resources may not contain components with the same UIDs and no Recurrence-IDs (%s and %s found)" % (component_id, subcomponent.propertyValue("UID"))
+ log.debug(msg)
+ raise ValueError(msg)
else:
got_master = True
master_recurring = subcomponent.hasProperty("RRULE") or subcomponent.hasProperty("RDATE")
@@ -1094,13 +1112,15 @@
# Check that if an override is present then the master is recurring
if got_override and got_master and not master_recurring:
- raise ValueError("Calendar resources must have a recurring master component if there is an overridden one " +
- "(%s)" % (subcomponent.propertyValue("UID"),))
+ msg = "Calendar resources must have a recurring master component if there is an overridden one (%s)" % (subcomponent.propertyValue("UID"),)
+ log.debug(msg)
+ raise ValueError(msg)
# Check for duplicate RECURRENCE-IDs
if rid in component_rids:
- raise ValueError("Calendar resources may not contain components with the same Recurrence-IDs " +
- "(%s)" % (rid,))
+ msg = "Calendar resources may not contain components with the same Recurrence-IDs (%s)" % (rid,)
+ log.debug(msg)
+ raise ValueError(msg)
else:
component_rids.add(rid)
@@ -1111,7 +1131,9 @@
#
for timezone_ref in timezone_refs:
if timezone_ref not in timezones:
- raise ValueError("Timezone ID %s is referenced but not defined" % (timezone_ref,))
+ msg = "Timezone ID %s is referenced but not defined: %s" % (timezone_ref, self,)
+ log.debug(msg)
+ raise ValueError(msg)
#
# FIXME:
@@ -1120,8 +1142,7 @@
#
for timezone in timezones:
if timezone not in timezone_refs:
- #raise ValueError(
- log.msg(
+ log.debug(
"Timezone %s is not referenced by any non-timezone component" % (timezone,)
)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2009-02-16 01:17:37 UTC (rev 3672)
@@ -14,6 +14,7 @@
# limitations under the License.
##
+from twistedcaldav.dateops import normalizeToUTC
from twistedcaldav.ical import Component, Property
from twistedcaldav.log import Logger
from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
@@ -112,10 +113,14 @@
# Find matching component in new calendar
new_component = self.calendar2.overriddenComponent(rid)
if new_component is None:
+ # If the old component was cancelled ignore when an attendee
+ if not is_organizer and old_component.propertyValue("STATUS") == "CANCELLED":
+ continue
+
# Determine whether the instance is still valid in the new calendar
- if True:
+ new_component = self.calendar2.deriveInstance(rid)
+ if new_component:
# 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:
@@ -137,6 +142,10 @@
# Find matching component in old calendar
old_component = self.calendar1.overriddenComponent(rid)
if old_component is None:
+ # If the new component is cancelled ignore when an attendee
+ if not is_organizer and new_component.propertyValue("STATUS") == "CANCELLED":
+ continue
+
# Try to derive a new instance in the client and transfer attendee status
old_component = self.calendar1.deriveInstance(rid)
if old_component:
@@ -242,14 +251,14 @@
self.calendar2 = duplicateAndNormalize(self.calendar2)
if self.calendar1 == self.calendar2:
- return True, True, ()
+ return True, True
# Need to look at each component and do special comparisons
# Make sure the same VCALENDAR properties match
if not self._checkVCALENDARProperties():
self._logDiffError("attendeeDiff: VCALENDAR properties do not match")
- return False, False, ()
+ return False, False
# Make sure the same VTIMEZONE components appear
tzidRemapping = False
@@ -271,7 +280,7 @@
self._logDiffError("attendeeDiff: VTIMEZONEs re-mapped")
except ValueError, e:
self._logDiffError("attendeeDiff: VTIMEZONE re-mapping failed: %s" % (str(e),))
- return False, False, ()
+ return False, False
return result
@@ -414,6 +423,7 @@
# The following statement is required to force vobject to serialize the
# calendar data and in the process add any missing VTIMEZONEs as needed.
_ignore = str(self.originalCalendar2)
+ log.debug(_ignore)
def _remapTZIDsOnComponent(self, oldComponent, newComponent):
"""
@@ -483,6 +493,8 @@
# First get uid/rid map of components
def mapComponents(calendar):
map = {}
+ cancelledRids = set()
+ master = None
for component in calendar.subcomponents():
if component.name() == "VTIMEZONE":
continue
@@ -490,63 +502,80 @@
uid = component.propertyValue("UID")
rid = component.getRecurrenceIDUTC()
map[(name, uid, rid,)] = component
- return map
+ if component.propertyValue("STATUS") == "CANCELLED" and rid is not None:
+ cancelledRids.add(rid)
+ if rid is None:
+ master = component
+
+ # Normalize each master by adding any STATUS:CANCELLED components as EXDATEs
+ exdates = set()
+ if master:
+ for rid in sorted(cancelledRids):
+ master.addProperty(Property("EXDATE", [rid,]))
+
+ # Get all EXDATEs in UTC
+ for exdate in master.properties("EXDATE"):
+ exdates.update([normalizeToUTC(value) for value in exdate.value()])
+
+ return exdates, map
- map1 = mapComponents(self.calendar1)
+ exdates1, map1 = mapComponents(self.calendar1)
set1 = set(map1.keys())
- map2 = mapComponents(self.calendar2)
+ exdates2, map2 = mapComponents(self.calendar2)
set2 = set(map2.keys())
- # Ugly case: if an Attendee has a STATUS:CANCELLED meeting and the ORGANIZER does not,
- # we may need to remove an EXDATE for the cancelled instance from the ORGANIZER's
- # master instance to ensure that matches
- cancelled_rids = []
- master2 = self.calendar2.masterComponent()
- for key in set2 - set1:
- component2 = map2[key]
- if component2.propertyValue("STATUS") == "CANCELLED":
- rid = component2.getRecurrenceIDUTC()
- cancelled_rids.append(rid)
- if master2:
- master2.addProperty(Property("EXDATE", [rid,]))
-
- # All the components in calendar1 must be in calendar2
+ # All the components in calendar1 must be in calendar2 unless they are CANCELLED
result = set1 - set2
- if result:
- log.debug("Missing components from first calendar: %s" % (result,))
- return False, False, ()
+ for key in result:
+ component = map1[key]
+ if component.propertyValue("STATUS") != "CANCELLED":
+ log.debug("Missing uncancelled component from first calendar: %s" % (key,))
+ return False, False
+ else:
+ _ignore_name, _ignore_uid, rid = key
+ if rid not in exdates2:
+ log.debug("Missing EXDATE for cancelled components from first calendar: %s" % (key,))
+ return False, False
+
# Now verify that each component in set1 matches what is in set2
attendee_unchanged = True
for key, value in map1.iteritems():
component1 = value
- component2 = map2[key]
+ component2 = map2.get(key)
+ if component2 is None:
+ continue
nomismatch, no_attendee_change = self._testComponents(component1, component2)
if not nomismatch:
- return False, False, ()
+ return False, False
attendee_unchanged &= no_attendee_change
# Now verify that each additional component in set2 matches a derived component in set1
for key in set2 - set1:
- # First check if the attendee's copy is cancelled
+ # First check if the attendee's copy is cancelled and properly EXDATE'd
+ # and skip it if so.
component2 = map2[key]
if component2.propertyValue("STATUS") == "CANCELLED":
+ _ignore_name, _ignore_uid, rid = key
+ if rid not in exdates1:
+ log.debug("Cancelled component not found in first calendar (or no EXDATE): %s" % (key,))
+ return False, False
continue
# Now derive the organizer's expected instance and compare
component1 = self.calendar1.deriveInstance(key[2])
if component1 is None:
log.debug("_compareComponents: Could not derive instance: %s" % (key[2],))
- return False, False, ()
+ return False, False
nomismatch, no_attendee_change = self._testComponents(component1, component2)
if not nomismatch:
- return False, False, ()
+ return False, False
attendee_unchanged &= no_attendee_change
- return True, attendee_unchanged, tuple(cancelled_rids)
+ return True, attendee_unchanged
def _testComponents(self, comp1, comp2):
Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2009-02-16 01:17:37 UTC (rev 3672)
@@ -648,8 +648,12 @@
yield self.doAccessControl(self.attendeePrincipal, False)
if self.action == "remove":
- log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % (self.attendee, self.uid))
- yield self.scheduleCancelWithOrganizer()
+ if self.calendar.hasPropertyValueInAllComponents(Property("STATUS", "CANCELLED")):
+ log.debug("Implicit - attendee '%s' is removing cancelled UID: '%s'" % (self.attendee, self.uid))
+ # Nothing else to do
+ else:
+ log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % (self.attendee, self.uid))
+ yield self.scheduleCancelWithOrganizer()
else:
# Get the ORGANIZER's current copy of the calendar object
@@ -662,7 +666,7 @@
self.oldcalendar = None
# Determine whether the current change is allowed
- change_allowed, no_itip, cancelled_rids = self.isAttendeeChangeInsignificant()
+ change_allowed, no_itip = self.isAttendeeChangeInsignificant()
if not change_allowed:
if self.calendar.hasPropertyValueInAllComponents(Property("STATUS", "CANCELLED")):
@@ -673,21 +677,6 @@
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")))
- # Remove orphaned attendee cancelled events
- if cancelled_rids:
- log.debug("Attendee '%s' is creating CANCELLED overridden instances for UID: '%s' - removing instances" % (self.attendee, self.uid,))
- master = self.calendar.masterComponent()
- for rid in cancelled_rids:
- self.calendar.removeComponent(self.calendar.overriddenComponent(rid))
- if master:
- master.addProperty(Property("EXDATE", [rid,]))
-
- # If no components left, make sure we delete the orphaned event
- if self.calendar.mainType() is None:
- log.debug("Attendee '%s' CANCELLED all instances of UID: '%s' - removing entire event" % (self.attendee, self.uid,))
- self.return_status = ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT
- returnValue(None)
-
if no_itip:
log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
returnValue(None)
@@ -758,10 +747,8 @@
oldcalendar = self.organizer_calendar
oldcalendar.attendeesView((self.attendee,))
differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
- change_allowed, no_itip, cancelled_rids = differ.attendeeDiff(self.attendee)
+ return differ.attendeeDiff(self.attendee)
- return change_allowed, no_itip, cancelled_rids
-
def scheduleWithOrganizer(self):
itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2009-02-16 01:17:37 UTC (rev 3672)
@@ -33,6 +33,7 @@
import datetime
from twistedcaldav.config import config
+from twistedcaldav.dateops import normalizeToUTC
from twistedcaldav.log import Logger
from twistedcaldav.ical import Property, iCalendarProductID, Component
@@ -118,6 +119,24 @@
if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
iTipProcessing.transferItems(calendar, master_valarms, private_comments, component)
+ # Now try to match recurrences
+ for component in calendar.subcomponents():
+ if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
+ rid = component.getRecurrenceIDUTC()
+ if new_calendar.overriddenComponent(rid) is None:
+ new_component = new_calendar.deriveInstance(rid)
+ new_calendar.addComponent(new_component)
+ iTipProcessing.transferItems(calendar, master_valarms, private_comments, new_component)
+ if component.propertyValue("STATUS") == "CANCELLED":
+ new_component.replaceProperty(Property("STATUS", "CANCELLED"))
+ for exdate in master_component.properties("EXDATE"):
+ for value in exdate.value():
+ if value == rid:
+ exdate.value().remove(value)
+ if len(exdate.value()) == 0:
+ master_component.removeProperty(exdate)
+ break
+
# Replace the entire object
return new_calendar, props_changed, rids
@@ -142,7 +161,7 @@
return calendar, props_changed, rids
@staticmethod
- def processCancel(itip_message, calendar):
+ def processCancel(itip_message, calendar, autoprocessing=False):
"""
Process a METHOD=CANCEL.
@@ -164,7 +183,13 @@
# Check to see if this is a cancel of the entire event
if itip_message.masterComponent() is not None:
- return True, True, None
+ if autoprocessing:
+ # Delete the entire event off the auto-processed calendar
+ return True, True, None
+ else:
+ # Cancel every instance in the existing event
+ calendar.replacePropertyInAllComponents(Property("STATUS", "CANCELLED"))
+ return True, False, None
# iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
# So we need to iterate over each iTIP component.
@@ -189,17 +214,28 @@
if overridden:
# We are cancelling an overridden component.
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
+ if autoprocessing:
+ # Exclude the cancelled instance
+ exdates.append(component.getRecurrenceIDUTC())
- # Remove the existing component.
- calendar.removeComponent(overridden)
+ # Remove the existing component.
+ calendar.removeComponent(overridden)
+ else:
+ # Existing component is cancelled.
+ overridden.replaceProperty(Property("STATUS", "CANCELLED"))
+
elif calendar_master:
# We are trying to CANCEL a non-overridden instance.
+
+ if autoprocessing:
+ # Exclude the cancelled instance
+ exdates.append(component.getRecurrenceIDUTC())
+ else:
+ # Derive a new component and cancel it.
+ overridden = calendar.deriveInstance(rid)
+ overridden.replaceProperty(Property("STATUS", "CANCELLED"))
+ calendar.addComponent(overridden)
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
# If we have any EXDATEs lets add them to the existing calendar object.
if exdates and calendar_master:
calendar_master.addProperty(Property("EXDATE", exdates))
@@ -461,7 +497,7 @@
comp.addProperty(Property("SEQUENCE", seq))
comp.addProperty(instance.getOrganizerProperty())
if instance_rid:
- comp.addProperty(Property("RECURRENCE-ID", instance_rid))
+ comp.addProperty(Property("RECURRENCE-ID", normalizeToUTC(instance_rid)))
def addProperties(propname):
for property in instance.properties(propname):
Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2009-02-15 19:10:03 UTC (rev 3671)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2009-02-16 01:17:37 UTC (rev 3672)
@@ -369,7 +369,7 @@
autoprocessed = self.recipient.principal.autoSchedule()
# Check to see if this is a cancel of the entire event
- processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar)
+ processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar, autoprocessing=autoprocessed)
if processed_message:
if delete_original:
@@ -393,14 +393,14 @@
yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
# Build the schedule-changes XML element
+ actions = (customxml.Cancel(),)
+ if rids:
+ actions += (customxml.Recurrences(
+ *[customxml.RecurrenceID.fromString(rid) for rid in rids]
+ ),)
changes = customxml.ScheduleChanges(
customxml.DTStamp(),
- customxml.Action(
- customxml.Cancel(),
- customxml.Recurrences(
- *[customxml.RecurrenceID.fromString(rid) for rid in rids]
- ),
- ),
+ customxml.Action(*actions),
)
result = (True, autoprocessed, changes)
else:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090215/62aa3927/attachment-0001.html>
More information about the calendarserver-changes
mailing list