[CalendarServer-changes] [9746] CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Sat Aug 25 07:27:26 PDT 2012
Revision: 9746
http://trac.macosforge.org/projects/calendarserver/changeset/9746
Author: cdaboo at apple.com
Date: 2012-08-25 07:27:26 -0700 (Sat, 25 Aug 2012)
Log Message:
-----------
Checkpoint of work so far.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/__init__.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/customxml.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/datafilters/peruserdata.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/datafilters/test/test_peruserdata.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/ical.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/method/post.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/method/put_common.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/scheduling/icaldiff.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/stdconfig.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/storebridge.py
CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/test/test_icalendar.py
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/__init__.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/__init__.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -41,6 +41,7 @@
import twistedcaldav.carddavxml
import twistedcaldav.mkcolxml
import twistedcaldav.customxml
+import twistedcaldav.timezonexml
twistedcaldav # Shhh.. pyflakes
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/customxml.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/customxml.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -69,6 +69,11 @@
)
+calendarserver_recurrence_split_compliance = (
+ "calendarserver-recurrence-split",
+)
+
+
@registerElement
class TwistedCalendarSupportedComponents (WebDAVTextElement):
"""
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/datafilters/peruserdata.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/datafilters/peruserdata.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/datafilters/peruserdata.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -140,6 +140,80 @@
self._mergeRepresentations(icalnew, icalold)
return icalnew
+ def extract(self, icalold, fromRID, newUID):
+ """
+ Return an iCalendar object with just per-user components not including ones for
+ the specified user UUID, and only including the master component and RIDs on or after
+ the specified RID. Also, adjust to a new UID if needed.
+
+ This method is used to aid recurrence splitting where we need to preserve per-user data in the
+ original component when it is split.
+
+ @param icalold: existing calendar data
+ @type icalold: L{Component} or C{str}
+ @param fromRID: RID that starts per-user components
+ @type fromRID: L{PyCalendarDateTime}
+ @param newUID: new iCalendar UID to use or None to leave unchanged
+ @type newUID: C{str} or C{None}
+ """
+
+ icalold = self.validCalendar(icalold)
+ extracted = icalold.duplicate()
+ for component in tuple(extracted.subcomponents()):
+ if component.name() != PerUserDataFilter.PERUSER_COMPONENT:
+ # Remove unwanted components
+ extracted.removeComponent(component)
+ else:
+ uuid = component.propertyValue(PerUserDataFilter.PERUSER_UID)
+ if uuid == self.uid:
+ extracted.removeComponent(component)
+ else:
+ # Adjust to any new UID
+ if newUID:
+ component.getProperty("UID").setValue(newUID)
+
+ # Now filter out per instance components before the RID
+ for perinstance in tuple(component.subcomponents()):
+ if perinstance.name() != PerUserDataFilter.PERINSTANCE_COMPONENT:
+ raise AssertionError("Wrong sub-component '%s' in a X-CALENDARSERVER-PERUSER component" % (perinstance.name(),))
+ rid = perinstance.getRecurrenceIDUTC()
+ if rid and rid < fromRID:
+ component.removeComponent(perinstance)
+
+ return extracted
+
+ def truncateRecurrenceDueToSplit(self, calendar, splitRID):
+ """
+ Remove overridden components, per-user data at or beyond the specified RECURRENCE-ID.
+
+ @param calendar: calendar to operate on
+ @type calendar: L{Component} or C{str}
+ @param splitRID: RID to remove from
+ @type splitRID: L{PyCalendarDateTime}
+ """
+
+ calendar = self.validCalendar(calendar)
+
+ # First do RRULE, override truncation
+ calendar.truncateRecurrenceDueToSplit(splitRID)
+
+ # Now do per-user data
+ for component in tuple(calendar.subcomponents()):
+ if component.name() == PerUserDataFilter.PERUSER_COMPONENT:
+ # Now filter out per instance components on or after the RID
+ for perinstance in tuple(component.subcomponents()):
+ if perinstance.name() != PerUserDataFilter.PERINSTANCE_COMPONENT:
+ raise AssertionError("Wrong sub-component '%s' in a X-CALENDARSERVER-PERUSER component" % (perinstance.name(),))
+ rid = perinstance.getRecurrenceIDUTC()
+ if rid and rid >= splitRID:
+ component.removeComponent(perinstance)
+
+ # If none left, remove the overall per-user data
+ if len(tuple(component.subcomponents())) == 0:
+ calendar.removeComponent(component)
+
+ return calendar
+
def _mergeBack(self, ical, peruser):
"""
Merge the per-user data back into the main calendar data.
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/datafilters/test/test_peruserdata.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/datafilters/test/test_peruserdata.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/datafilters/test/test_peruserdata.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -17,6 +17,7 @@
import twistedcaldav.test.util
from twistedcaldav.ical import Component
from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
+from pycalendar.datetime import PyCalendarDateTime
dataForTwoUsers = """BEGIN:VCALENDAR
VERSION:2.0
@@ -6258,3 +6259,472 @@
self.assertEqual(str(filtered), result)
unfiltered = PerUserDataFilter("user01").filter(filtered)
self.assertEqual(str(unfiltered), unfiltered_result)
+
+
+class PerUserDataExtractTest (twistedcaldav.test.util.TestCase):
+
+ def test_extract(self):
+
+ data = (
+ # tuple (old_data, split_at_rid, new_uid, result)
+
+ # No peruser data
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+ PyCalendarDateTime.parseText("20120822T120000Z"),
+ "DEF",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+ ),
+
+ # Only owner peruser data
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+ PyCalendarDateTime.parseText("20120822T120000Z"),
+ "DEF",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+ ),
+
+ # Other peruser data - master only
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+ PyCalendarDateTime.parseText("20120822T120000Z"),
+ "DEF",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:DEF
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+ ),
+
+ # Other peruser data - master + three instances
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120821T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02-20120821T120000Z
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120822T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02-20120822T120000Z
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120824T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02-20120824T120000Z
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+ PyCalendarDateTime.parseText("20120822T120000Z"),
+ "DEF",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:DEF
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120822T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02-20120822T120000Z
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120824T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02-20120824T120000Z
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+ ),
+ )
+
+ for old_data, split_rid, new_uid, result in data:
+ extracted = PerUserDataFilter("user01").extract(old_data, split_rid, new_uid)
+ self.assertEqual(str(extracted), result)
+
+
+class PerUserDataTruncateTest (twistedcaldav.test.util.TestCase):
+
+ def test_split(self):
+
+ data = (
+ # tuple (input_data, split_at_rid, result)
+
+ # No peruser data
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20120822T120000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;UNTIL=20120822T115959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+
+ # Peruser data - master only
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20120822T120000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;UNTIL=20120822T115959Z
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test user02
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+ ),
+
+ # Peruser data - three overrides
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120821T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120822T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120822T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2mod2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120823T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2mod2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20120822T120000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20120820T120000Z
+DURATION:PT1H
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;UNTIL=20120822T115959Z
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20120821T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for old_data, split_rid, result in data:
+ split = PerUserDataFilter("").truncateRecurrenceDueToSplit(old_data.replace("\n", "\r\n"), split_rid)
+ self.assertEqual(str(split), result.replace("\n", "\r\n"))
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/ical.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/ical.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -952,6 +952,61 @@
self._markAsDirty()
return changed
+ def truncateRecurrenceDueToSplit(self, splitRID):
+ """
+ Truncate RRULEs etc to make sure there are no more than the given number
+ of instances. This assumes that the RID has already been validated as an
+ existing, valid instance.
+
+ @param splitRID: the recurrence-id where the split occurs
+ @type maximumCount: L{PyCalendarDateTime}
+ """
+
+ splitRID = splitRID.duplicateAsUTC()
+
+ master = self.masterComponent()
+ if master and master.isRecurring():
+ # Force all RRULEs to an UNTIL one second before the RID
+ rrules = master._pycalendar.getRecurrenceSet()
+ if rrules:
+ until = splitRID.duplicate()
+ until.offsetSeconds(-1)
+ for rrule in rrules.getRules():
+ rrule.setUseCount(False)
+ rrule.setUseUntil(True)
+ rrule.setUntil(until)
+
+ # Remove RDATES on or after RID
+ for rdate in tuple(master.properties("RDATE")):
+ for value in tuple(rdate.value()):
+ if isinstance(value.getValue(), PyCalendarDateTime):
+ if value.getValue() >= splitRID:
+ rdate.value().remove(value)
+ elif isinstance(value.getValue(), PyCalendarPeriod):
+ if value.getValue().getStart() >= splitRID:
+ rdate.value().remove(value)
+ if len(rdate.value()) == 0:
+ master.removeProperty(rdate)
+
+ # Remove EXDATES on or after RID
+ for exdate in tuple(master.properties("EXDATE")):
+ for value in tuple(exdate.value()):
+ if value.getValue() >= splitRID:
+ exdate.value().remove(value)
+ if len(exdate.value()) == 0:
+ master.removeProperty(exdate)
+
+ # After creating/changing a component we need to do this to keep PyCalendar happy
+ master._pycalendar.finalise()
+
+ # Remove overridden components
+ for component in tuple(self.subcomponents()):
+ rid = component.getRecurrenceIDUTC()
+ if rid is not None and rid >= splitRID:
+ self.removeComponent(component)
+
+ self._markAsDirty()
+
def expand(self, start, end, timezone=None):
"""
Expand the components into a set of new components, one for each
@@ -1145,6 +1200,22 @@
return True
return False
+ def recurrenceSet(self):
+ """
+ @return: the RECURRENCE-SET of the subcomponents in this component.
+ """
+ assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+ if not hasattr(self, "_recurrence_set"):
+ for subcomponent in self.subcomponents():
+ if subcomponent.name() not in ignoredComponents:
+ self._recurrence_set = subcomponent.propertyValue("RECURRENCE-SET")
+ break
+ else:
+ self._recurrence_set = None
+
+ return self._recurrence_set
+
def deriveInstance(self, rid, allowCancelled=False, newcomp=None):
"""
Derive an instance from the master component that has the provided RECURRENCE-ID, but
@@ -2823,6 +2894,26 @@
return tuple(results)
+ def peruserComponents(self, ignoreUUID, fromRID, newUID):
+ """
+ Return an iCalendar object with just per-user components (not including ones for
+ the specified user UUID, and only including the master component and RIDs on or after
+ the specified RID.
+
+ @param ignoreUID: UUID of user to ignore
+ @type ignoreUID: C{str}
+ @param fromRID: RID that starts per-user components
+ @type fromRID: L{PyCalendarDateTime}
+ @param newUID: new iCalendar UID to use or None to leave unchanged
+ @type newUID: C{str} or C{None}
+ """
+
+ results = set()
+ for component in self.subcomponents():
+ if component.name() == "X-CALENDARSERVER-PERUSER":
+ results.add(component.propertyValue("X-CALENDARSERVER-PERUSER-UID"))
+ return results
+
def hasInstancesAfter(self, limit):
"""
Determine whether an event exists completely prior to a given moment.
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/method/post.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/method/post.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/method/post.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -45,24 +45,38 @@
# POST can support many different APIs
- # First look at query params
- if request.params:
- if request.params == "add-member":
- if config.EnableAddMember:
- result = (yield POST_handler_add_member(self, request))
- returnValue(result)
+ # Look for action argument
+ if request.args and "action" in request.args:
+ result = (yield POST_handler_action(self, request))
+ returnValue(result)
- else:
- # Content-type handlers
- contentType = request.headers.getHeader("content-type")
- if contentType:
- if hasattr(self, "POST_handler_content_type"):
- result = (yield self.POST_handler_content_type(request, (contentType.mediaType, contentType.mediaSubtype)))
- returnValue(result)
+ # Check for add-member query param
+ if request.params and request.params == "add-member" and config.EnableAddMember:
+ result = (yield POST_handler_add_member(self, request))
+ returnValue(result)
+ # Content-type handlers
+ contentType = request.headers.getHeader("content-type")
+ if contentType and hasattr(self, "POST_handler_content_type"):
+ result = (yield self.POST_handler_content_type(request, (contentType.mediaType, contentType.mediaSubtype)))
+ returnValue(result)
+
returnValue(responsecode.FORBIDDEN)
@inlineCallbacks
+def POST_handler_action(self, request):
+
+ action = request.args["action"][0]
+ if hasattr(self, "POSTactionDispatch") and action in self.POSTactionDispatch:
+ result = (yield self.POSTactionDispatch[action](self, request))
+ returnValue(result)
+
+ raise HTTPError(StatusResponse(
+ responsecode.BAD_REQUEST,
+ "No POST action handler for '%s' on this resource" % (action,),
+ ))
+
+ at inlineCallbacks
def POST_handler_add_member(self, request):
# Handle ;add-member
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/method/put_common.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/method/put_common.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -133,6 +133,7 @@
internal_request=False,
processing_organizer=None,
returnData=False,
+ perUserMergeData=None,
):
"""
Function that does common PUT/COPY/MOVE behavior.
@@ -155,6 +156,7 @@
@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.
@param returnData: True if the caller wants the actual data written to the store returned
+ @param perUserMergeData: L{Component} for per user data to merge in
"""
# Check that all arguments are valid
@@ -196,19 +198,27 @@
self.internal_request = internal_request
self.processing_organizer = processing_organizer
self.returnData = returnData
+ self.perUserMergeData = perUserMergeData
self.access = None
self.hasPrivateComments = False
self.isScheduleResource = False
+ def partialValidation(self):
+ """
+ Do a partial validation of source and destination calendar data.
+ """
+ return self.fullValidation(True)
+
@inlineCallbacks
- def fullValidation(self):
+ def fullValidation(self, partial=False):
"""
Do full validation of source and destination calendar data.
"""
- # Basic validation
- self.validIfScheduleMatch()
+ # Basic validation - we skip this for the partial case
+ if not partial:
+ self.validIfScheduleMatch()
if self.destinationcal:
@@ -984,7 +994,9 @@
if self.calendar:
accessUID = (yield self.destination.resourceOwnerPrincipal(self.request))
accessUID = accessUID.principalUID() if accessUID else ""
- if self.destination.exists() and self.destinationcal:
+ if self.perUserMergeData is not None:
+ oldCal = self.perUserMergeData
+ elif self.destination.exists() and self.destinationcal:
oldCal = yield self.destination.iCalendar()
else:
oldCal = None
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/scheduling/icaldiff.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/scheduling/icaldiff.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -595,8 +595,8 @@
rdates = component.properties("RDATE")
for rdate in rdates:
for value in rdate.value():
- if isinstance(value, PyCalendarDateTime):
- value = value.duplicate().adjustToUTC()
+ if isinstance(value.getValue(), PyCalendarDateTime):
+ value = value.getValue().duplicate().adjustToUTC()
newrdates.add(value)
# EXDATEs
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/stdconfig.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/stdconfig.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -533,6 +533,8 @@
"MaxResourcesBatchUpload" : 100, # Maximum number of resources in a batch POST
"MaxBytesBatchUpload" : 10485760, # Maximum size of a batch POST (10 MB)
+ "EnableRecurrenceSplitting" : True, # POST to trigger recurrence split
+
"Sharing": {
"Enabled" : False, # Overall on/off switch
"AllowExternalUsers" : False, # External (non-principal) sharees allowed
@@ -1457,6 +1459,8 @@
compliance += caldavxml.caldav_query_extended_compliance
if configDict.EnableDefaultAlarms:
compliance += caldavxml.caldav_default_alarms_compliance
+ if configDict.EnableRecurrenceSplitting:
+ compliance += customxml.calendarserver_recurrence_split_compliance
else:
compliance = ()
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/storebridge.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/storebridge.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -54,7 +54,8 @@
from twistedcaldav.carddavxml import carddav_namespace
from twistedcaldav.config import config
from twistedcaldav.ical import Component as VCalendar, Property as VProperty,\
- InvalidICalendarDataError, iCalendarProductID, allowedComponents
+ InvalidICalendarDataError, iCalendarProductID, allowedComponents, Component,\
+ Property
from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
from twistedcaldav.method.put_addressbook_common import StoreAddressObjectResource
from twistedcaldav.method.put_common import StoreCalendarObjectResource
@@ -66,6 +67,10 @@
from twistedcaldav.schedule import ScheduleInboxResource
from twistedcaldav.scheduling.implicit import ImplicitScheduler
from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
+from twistedcaldav.customxml import calendarserver_namespace
+import uuid
+from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
+from pycalendar.datetime import PyCalendarDateTime
"""
Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
@@ -115,9 +120,9 @@
FORBIDDEN,
"Property cannot be changed: %s" % (property.sname(),)
))
-
+
def delete(self, qname, uid=None):
try:
del self._newPropertyStore[self._convertKey(qname)]
@@ -232,7 +237,7 @@
def liveProperties(self):
props = super(_CommonHomeChildCollectionMixin, self).liveProperties()
-
+
if config.MaxResourcesPerCollection:
props += (customxml.MaxResources.qname(),)
@@ -297,14 +302,14 @@
if self._newStoreObject:
newStoreObject = yield self._newStoreObject.objectResourceWithName(name)
-
+
similar = self._childClass(
newStoreObject,
self._newStoreObject,
name,
principalCollections=self._principalCollections
)
-
+
self.propagateTransaction(similar)
returnValue(similar)
else:
@@ -360,26 +365,26 @@
"""
Override to pre-load children in certain collection types for better performance.
"""
-
+
if depth == "1":
if names:
yield self._newStoreObject.objectResourcesWithNames(names)
else:
yield self._newStoreObject.objectResources()
-
+
result = (yield super(_CommonHomeChildCollectionMixin, self).findChildrenFaster(
depth, request, okcallback, badcallback, missingcallback, names, privileges, inherited_aces
))
-
+
returnValue(result)
-
+
@inlineCallbacks
def createCollection(self):
"""
Override C{createCollection} to actually do the work.
"""
self._newStoreObject = (yield self._newStoreParentHome.createChildWithName(self._name))
-
+
# Re-initialize to get stuff setup again now we have a "real" object
self._initializeWithHomeChild(self._newStoreObject, self._parentResource)
@@ -389,7 +394,7 @@
@inlineCallbacks
def http_DELETE(self, request):
"""
- Override http_DELETE to validate 'depth' header.
+ Override http_DELETE to validate 'depth' header.
"""
if not self.exists():
@@ -473,7 +478,7 @@
# Actually delete it.
yield self._newStoreObject.remove()
-
+
# Re-initialize to get stuff setup again now we have no object
self._initializeWithHomeChild(None, self._parentResource)
@@ -515,7 +520,7 @@
destination = yield request.locateResource(destinationURI)
if destination.exists():
returnValue(FORBIDDEN)
-
+
# Forget the destination now as after the move we will need to re-init it with its
# new store object
request._forgetResource(destination, destinationURI)
@@ -566,31 +571,31 @@
@requiresPermissions(davxml.Bind())
@inlineCallbacks
def simpleBatchPOST(self, request):
-
+
# If CTag precondition
yield self.checkCTagPrecondition(request)
-
+
# Look for return changed data option
return_changed = self.checkReturnChanged(request)
# Read in all data
data = (yield allDataFromStream(request.stream))
-
+
components = self.componentsFromData(data)
if components is None:
raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body"))
-
+
# Build response
xmlresponses = []
for ctr, component in enumerate(components):
-
+
code = None
error = None
dataChanged = None
try:
# Create a new name if one was not provided
name = md5(str(ctr) + component.resourceUID() + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
-
+
# Get a resource for the new item
newchildURL = joinURL(request.path, name)
newchild = (yield request.locateResource(newchildURL))
@@ -604,9 +609,9 @@
error = (error.namespace, error.name,)
except Exception:
code = responsecode.BAD_REQUEST
-
+
if code is None:
-
+
etag = (yield newchild.etag())
if not return_changed or dataChanged is None:
xmlresponses.append(
@@ -634,7 +639,7 @@
)
)
)
-
+
else:
xmlresponses.append(
davxml.StatusResponse(
@@ -646,9 +651,9 @@
) if error else None,
)
)
-
+
result = MultiStatusResponse(xmlresponses)
-
+
newctag = (yield self.getInternalSyncToken())
result.headers.setRawHeaders("CTag", (newctag,))
@@ -659,16 +664,16 @@
request.extendedLogItems["rcount"] = len(xmlresponses)
returnValue(result)
-
+
@inlineCallbacks
def crudBatchPOST(self, request, xmlroot):
-
+
# Need to force some kind of overall authentication on the request
yield self.authorize(request, (davxml.Read(), davxml.Write(),))
# If CTag precondition
yield self.checkCTagPrecondition(request)
-
+
# Look for return changed data option
return_changed = self.checkReturnChanged(request)
@@ -680,7 +685,7 @@
updateCount = 0
deleteCount = 0
for xmlchild in xmlroot.children:
-
+
# Determine the multiput operation: create, update, delete
href = xmlchild.childOfType(davxml.HRef.qname())
set = xmlchild.childOfType(davxml.Set.qname())
@@ -688,11 +693,11 @@
xmldata_root = prop if prop else set
xmldata = xmldata_root.childOfType(self.xmlDataElementType().qname()) if xmldata_root is not None else None
if href is None:
-
+
if xmldata is None:
raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Could not parse valid data from request body without a DAV:Href present"))
-
- # Do privilege check on collection once
+
+ # Do privilege check on collection once
if checkedBindPrivelege is None:
try:
yield self.authorize(request, (davxml.Bind(),))
@@ -716,7 +721,7 @@
yield self.crudUpdate(request, str(href), xmldata, ifmatch, return_changed, xmlresponses)
updateCount += 1
else:
- # Do privilege check on collection once
+ # Do privilege check on collection once
if checkedUnbindPrivelege is None:
try:
yield self.authorize(request, (davxml.Unbind(),))
@@ -726,9 +731,9 @@
yield self.crudDelete(request, str(href), ifmatch, xmlresponses, checkedUnbindPrivelege);
deleteCount += 1
-
+
result = MultiStatusResponse(xmlresponses)
-
+
newctag = (yield self.getInternalSyncToken())
result.headers.setRawHeaders("CTag", (newctag,))
@@ -748,7 +753,7 @@
@inlineCallbacks
def crudCreate(self, request, xmldata, xmlresponses, return_changed, hasPrivilege):
-
+
code = None
error = None
try:
@@ -760,7 +765,7 @@
# Create a new name if one was not provided
name = md5(str(componentdata) + str(time.time()) + request.path).hexdigest() + self.resourceSuffix()
-
+
# Get a resource for the new item
newchildURL = joinURL(request.path, name)
newchild = (yield request.locateResource(newchildURL))
@@ -777,7 +782,7 @@
except Exception:
code = responsecode.BAD_REQUEST
-
+
if code is None:
etag = (yield newchild.etag())
xmlresponses.append(
@@ -823,7 +828,7 @@
etag = (yield updateResource.etag())
if ifmatch and ifmatch != etag.generate():
raise HTTPError(responsecode.PRECONDITION_FAILED)
-
+
yield self.storeResourceData(request, updateResource, href, component, componentdata)
# FIXME: figure out return_changed behavior
@@ -837,7 +842,7 @@
except Exception:
code = responsecode.BAD_REQUEST
-
+
if code is None:
xmlresponses.append(
davxml.PropertyStatusResponse(
@@ -879,7 +884,7 @@
raise HTTPError(responsecode.PRECONDITION_FAILED)
yield deleteResource.storeRemove(
- request,
+ request,
True,
href,
)
@@ -893,7 +898,7 @@
except Exception:
code = responsecode.BAD_REQUEST
-
+
if code is None:
xmlresponses.append(
davxml.StatusResponse(
@@ -923,7 +928,7 @@
"""
Functions common to calendar and inbox collections
"""
-
+
# Support component set behaviors
def setSupportedComponentSet(self, support_components_property):
"""
@@ -931,7 +936,7 @@
"""
support_components = tuple([comp.attributes["name"].upper() for comp in support_components_property.children])
return self.setSupportedComponents(support_components)
-
+
def getSupportedComponentSet(self):
comps = self._newStoreObject.getSupportedComponents()
if comps:
@@ -949,14 +954,14 @@
@param components: list of names of components to support
@type components: C{list}
"""
-
+
# Validate them first - raise on failure
if not self.validSupportedComponents(components):
raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Invalid CALDAV:supported-calendar-component-set"))
support_components = ",".join(sorted([comp.upper() for comp in components]))
return maybeDeferred(self._newStoreObject.setSupportedComponents, support_components)
-
+
def getSupportedComponents(self):
comps = self._newStoreObject.getSupportedComponents()
if comps:
@@ -977,13 +982,13 @@
return components in (("VEVENT",), ("VTODO",),)
return True
-
+
class CalendarCollectionResource(DefaultAlarmPropertyMixin, _CalendarCollectionBehaviorMixin, _CommonHomeChildCollectionMixin, CalDAVResource):
"""
Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
"""
-
+
def __init__(self, calendar, home, name=None, *args, **kw):
"""
Create a CalendarCollectionResource from a L{txdav.caldav.icalendar.ICalendar}
@@ -1056,7 +1061,7 @@
assert subcalendar.name() == "VCALENDAR"
for component in subcalendar.subcomponents():
-
+
# Only insert VTIMEZONEs once
if component.name() == "VTIMEZONE":
tzid = component.propertyValue("TZID")
@@ -1081,7 +1086,7 @@
Need to split a single VCALENDAR into separate ones based on UID with the
appropriate VTIEMZONES included.
"""
-
+
results = []
# Split into components by UID and TZID
@@ -1097,14 +1102,14 @@
by_tzid[subcomponent.propertyValue("TZID")] = subcomponent
else:
by_uid.setdefault(subcomponent.propertyValue("UID"), []).append(subcomponent)
-
+
# Re-constitute as separate VCALENDAR objects
for components in by_uid.values():
-
+
newvcal = VCalendar("VCALENDAR")
newvcal.addProperty(VProperty("VERSION", "2.0"))
newvcal.addProperty(VProperty("PRODID", vcal.propertyValue("PRODID")))
-
+
# Get the set of TZIDs and include them
tzids = set()
for component in components:
@@ -1117,13 +1122,13 @@
# We ignore the error and generate invalid ics which someone will
# complain about at some point
pass
-
+
# Now add each component
for component in components:
newvcal.addComponent(component.duplicate())
-
+
results.append(newvcal)
-
+
return results
@classmethod
@@ -1147,10 +1152,10 @@
returnData = returnData,
)
yield storer.run()
-
+
returnValue(storer.storeddata if hasattr(storer, "storeddata") else None)
-
+
@inlineCallbacks
def storeRemove(self, request, implicitly, where):
"""
@@ -1211,7 +1216,7 @@
that calendar's name.
"""
defaultCalendarType = (yield self.isDefaultCalendar(request))
-
+
result = (yield super(CalendarCollectionResource, self).http_MOVE(request))
if result == NO_CONTENT:
destinationURI = urlsplit(request.headers.getHeader("destination"))[2]
@@ -1536,7 +1541,7 @@
# Invite shares use access mode from the invite
if record.state != "ACCEPTED":
continue
-
+
userprivs = [
]
if record.access in ("read-only", "read-write", "read-write-schedule",):
@@ -1644,7 +1649,7 @@
except IOError, e:
log.error("Unable to read attachment: %s, due to: %s" % (self, e,))
raise HTTPError(responsecode.NOT_FOUND)
-
+
return Response(OK, {"content-type":self.contentType()}, stream)
@@ -1667,7 +1672,7 @@
def http_PROPPATCH(self, request):
"""
- No dead properties allowed on attachments.
+ No dead properties allowed on attachments.
"""
return FORBIDDEN
@@ -1737,7 +1742,7 @@
@requiresPermissions(fromParent=[davxml.Unbind()])
def http_DELETE(self, request):
"""
- Override http_DELETE to validate 'depth' header.
+ Override http_DELETE to validate 'depth' header.
"""
if not self.exists():
log.debug("Resource not found: %s" % (self,))
@@ -1747,7 +1752,7 @@
def http_PROPPATCH(self, request):
"""
- No dead properties allowed on object resources.
+ No dead properties allowed on object resources.
"""
if self._newStoreParent.objectResourcesHaveProperties():
return super(_CommonObjectResource, self).http_PROPPATCH(request)
@@ -1851,7 +1856,7 @@
class _CalendarObjectMetaDataMixin(object):
"""
- Dynamically create the required meta-data for an object resource
+ Dynamically create the required meta-data for an object resource
"""
accessMode = _MetadataProperty("accessMode")
@@ -2012,6 +2017,267 @@
returnValue(NO_CONTENT)
+ @inlineCallbacks
+ def action_SplitRecurrence(self, request):
+ """
+ Split a recurring event into two at the specified RECURRENCE-ID, preserving per-attendee/per-user
+ data in the on-going event.
+ """
+
+ #
+ # Step 1: Various preconditions
+ #
+
+ # Feature must be enabled
+ if not config.EnableRecurrenceSplitting:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (calendarserver_namespace, "valid-recurrence-split"),
+ "Recurrence splitting is not enabled",
+ ))
+
+ # Not inbox
+ if self._newStoreObject._calendar.name() == "inbox":
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (calendarserver_namespace, "supported-recurrence-split"),
+ "Cannot split inbox resources",
+ ))
+
+ # Do If-Schedule-Tag-Match behavior first
+ # Important: this should only ever be done when called
+ # directly as a result of an HTTP POST to ensure the proper If-
+ # header is used in this test.
+ self.validIfScheduleMatch(request)
+
+ # Event must be recurring
+ oldcalendar = (yield self.iCalendar())
+ if not oldcalendar.isRecurring():
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (calendarserver_namespace, "supported-recurrence-split"),
+ "Cannot split non-recurring calendar resources",
+ ))
+
+ # Specified RECURRENCE-ID must exist and be valid
+ rid = request.args.get("recurrence-id", [None])[0]
+ if rid is None:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (calendarserver_namespace, "valid-recurrence-split"),
+ "Missing recurrence_id parameter",
+ ))
+ rid = PyCalendarDateTime.parseText(rid)
+ if not oldcalendar.validInstance(rid):
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (calendarserver_namespace, "valid-recurrence-split"),
+ "Invalid recurrence_id parameter - does not match a valid instance",
+ ))
+
+ # Must contain valid iCalendar data in the request body
+ content_type = request.headers.getHeader("content-type")
+ if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
+ log.err("MIME type %s not allowed in calendar collection" % (content_type,))
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "supported-calendar-data"),
+ "Invalid MIME type for calendar resource",
+ ))
+ newcalendar = (yield allDataFromStream(request.stream))
+ if not hasattr(request, "extendedLogItems"):
+ request.extendedLogItems = {}
+ request.extendedLogItems["cl"] = str(len(newcalendar)) if newcalendar else "0"
+
+ # We must have some data at this point
+ newcalendar = Component.fromString(newcalendar)
+ if newcalendar is None:
+ # Use correct DAV:error response
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "valid-calendar-data"),
+ "Valid calendar data must be present in the request"
+ ))
+
+ #
+ # Look for other events in the set that are after the split - those will need to be removed
+ #
+
+ #
+ # Step 2: Check for implicit scheduling and initiate ImplicitUID lock
+ #
+ pass
+
+ #
+ # Step 3: Save the new event with per-user data preserved
+ #
+
+ # Add the recurrence-set relation property
+ recurrenceSet = oldcalendar.recurrenceSet()
+ if recurrenceSet is None:
+ recurrenceSet = str(uuid.uuid4())
+ if newcalendar.recurrenceSet() is not None and newcalendar.recurrenceSet() != recurrenceSet:
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ (calendarserver_namespace, "valid-recurrence-split"),
+ "Cannot specify a RECURRENCE-SET property in the new data that is different from the original data",
+ ))
+ if newcalendar.recurrenceSet() is None:
+ newcalendar.addPropertyToAllComponents(Property("RECURRENCE-SET", recurrenceSet))
+
+ # Find current user's UID and extract others' per-user data from old calendar
+ accessUUID = (yield self.resourceOwnerPrincipal(request))
+ accessUUID = accessUUID.principalUID() if accessUUID else ""
+ oldperuser = PerUserDataFilter(accessUUID).extract(oldcalendar, rid, newcalendar.resourceUID())
+
+ # Get a resource for the new item
+ parentURL = parentForURL(request.path)
+ parent = (yield request.locateResource(parentURL))
+ newname = str(uuid.uuid4()) + ".ics"
+ newchildURL = joinURL(parentURL, newname)
+ newchild = yield request.locateResource(newchildURL)
+ newchild._url = newchildURL
+
+ # Check that the new data is valid for storing in this collection
+ yield StoreCalendarObjectResource(
+ request = request,
+ destination = newchild,
+ destination_uri = newchildURL,
+ destinationcal = True,
+ destinationparent = parent,
+ calendar = newcalendar,
+ allowImplicitSchedule = False,
+ perUserMergeData = oldperuser,
+ ).run()
+
+ #
+ # Step 4: Update existing event to truncate recurrence
+ #
+
+ # Truncate recurrence
+ oldcalendar = PerUserDataFilter("").truncateRecurrenceDueToSplit(oldcalendar, rid)
+
+ # Add the recurrence-set relation property
+ if oldcalendar.recurrenceSet() is None:
+ oldcalendar.addPropertyToAllComponents(Property("RECURRENCE-SET", recurrenceSet))
+
+ # Store the data back
+ yield self._newStoreObject.setComponent(oldcalendar)
+
+ #
+ # Step 5: Trigger implicit scheduling and release the lock when done
+ #
+ pass
+
+ #
+ # Step 6: Create response
+ #
+ # Return something that is equivalent to a multiget on the old and new resources with the
+ # client requesting getetag and option ally calendar-data (depending on whether Prefer is used).
+ #
+
+ # Look for Prefer header
+ prefer = request.headers.getHeader("prefer", {})
+ returnRepresentation = "return-representation" in prefer
+
+ # Build responses
+ xmlresponses = []
+
+ # Old resource
+ uri = request.path
+ etag = (yield self.etag())
+ props = (
+ davxml.GETETag.fromString(etag.generate()),
+ )
+ if returnRepresentation:
+ calendar = (yield self.iCalendarForUser(request))
+ props += (
+ caldavxml.CalendarData.fromCalendar(calendar),
+ )
+ xmlresponses.append(
+ davxml.PropertyStatusResponse(
+ davxml.HRef.fromString(uri),
+ davxml.PropertyStatus(
+ davxml.PropertyContainer(*props),
+ davxml.Status.fromResponseCode(responsecode.OK),
+ )
+ )
+ )
+
+ # New resource
+ uri = newchildURL
+ etag = (yield newchild.etag())
+ props = (
+ davxml.GETETag.fromString(etag.generate()),
+ )
+ if returnRepresentation:
+ calendar = (yield newchild.iCalendarForUser(request))
+ props += (
+ caldavxml.CalendarData.fromCalendar(calendar),
+ )
+ xmlresponses.append(
+ davxml.PropertyStatusResponse(
+ davxml.HRef.fromString(uri),
+ davxml.PropertyStatus(
+ davxml.PropertyContainer(*props),
+ davxml.Status.fromResponseCode(responsecode.CREATED),
+ )
+ )
+ )
+
+ result = MultiStatusResponse(xmlresponses)
+ returnValue(result)
+
+# # Now look for scheduling
+# scheduler = ImplicitScheduler()
+# do_implicit_action, _ignore = (
+# yield scheduler.testImplicitSchedulingSplitRecurrence(
+# request, self, oldcalendar
+# )
+# )
+# if do_implicit_action and scheduler.state != "organizer":
+# raise HTTPError(ErrorResponse(
+# responsecode.FORBIDDEN,
+# (caldav_namespace, "valid-attendee-change"),
+# "Cannot split recurrence",
+# ))
+#
+# if do_implicit_action:
+# lock = MemcacheLock(
+# "ImplicitUIDLock",
+# oldcalendar.resourceUID(),
+# timeout=config.Scheduling.Options.UIDLockTimeoutSeconds,
+# expire_time=config.Scheduling.Options.UIDLockExpirySeconds,
+# )
+#
+# try:
+# if lock:
+# yield lock.acquire()
+#
+# yield super(CalendarObjectResource, self).storeRemove(request)
+#
+# # Do scheduling
+# yield scheduler.doImplicitScheduling()
+#
+# except MemcacheLockTimeoutError:
+# raise HTTPError(StatusResponse(
+# CONFLICT,
+# "Resource: %s currently in use on the server." % (where,))
+# )
+#
+# finally:
+# if lock:
+# # Release lock after commit or abort
+# transaction.postCommit(lock.clean)
+# transaction.postAbort(lock.clean)
+
+ # POST actions
+ POSTactionDispatch = {
+ "recurrence-split": action_SplitRecurrence,
+ }
+
+
+
class AddressBookCollectionResource(_CommonHomeChildCollectionMixin, CalDAVResource):
"""
Wrapper around a L{txdav.carddav.iaddressbook.IAddressBook}.
@@ -2082,7 +2348,7 @@
returnData = returnData,
)
yield storer.run()
-
+
returnValue(storer.returndata if hasattr(storer, "returndata") else None)
@inlineCallbacks
@@ -2140,7 +2406,7 @@
that address book's name.
"""
defaultAddressBook = (yield self.isDefaultAddressBook(request))
-
+
result = (yield super(AddressBookCollectionResource, self).http_MOVE(request))
if result == NO_CONTENT:
destinationURI = urlsplit(request.headers.getHeader("destination"))[2]
@@ -2379,7 +2645,7 @@
@requiresPermissions(fromParent=[davxml.Unbind()])
def http_DELETE(self, request):
"""
- Override http_DELETE to validate 'depth' header.
+ Override http_DELETE to validate 'depth' header.
"""
if not self.exists():
log.debug("Resource not found: %s" % (self,))
@@ -2389,7 +2655,7 @@
def http_PROPPATCH(self, request):
"""
- No dead properties allowed on notification objects.
+ No dead properties allowed on notification objects.
"""
return FORBIDDEN
Modified: CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/test/test_icalendar.py 2012-08-25 14:25:38 UTC (rev 9745)
+++ CalendarServer/branches/users/cdaboo/recurrence-split/twistedcaldav/test/test_icalendar.py 2012-08-25 14:27:26 UTC (rev 9746)
@@ -4764,6 +4764,336 @@
elif changed:
self.fail("Truncation happened when not expected: %s" % (title,))
+ def test_truncate_recurrence_split(self):
+
+ data = (
+ (
+ "1.1 - no recurrence",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.2 - truncation - no count, no until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.3 - truncation - until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070105T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "1.4 - truncation - count",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY;COUNT=2000
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.1 - overrides, truncation - no count, no until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20070102T000000Z
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20070105T000000Z
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20070102T000000Z
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.2 - exdates, truncation - no count, no until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+EXDATE:20070102T000000Z
+EXDATE:20070105T000000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+EXDATE:20070102T000000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.3 - exdates list, truncation - no count, no until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+EXDATE:20070102T000000Z,20070105T000000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+EXDATE:20070102T000000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.4 - rdates, truncation - no count, no until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RDATE:20070102T080000Z
+RDATE:20070105T080000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RDATE:20070102T080000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.5 - rdates period, truncation - no count, no until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RDATE;VALUE=PERIOD:20070102T080000Z/PT2H
+RDATE;VALUE=PERIOD:20070105T080000Z/PT2H
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RDATE;VALUE=PERIOD:20070102T080000Z/PT2H
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.6 - rdates list, truncation - no count, no until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RDATE:20070102T080000Z,20070105T080000Z
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RDATE:20070102T080000Z
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "2.7 - rdates period list, truncation - no count, no until",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RDATE;VALUE=PERIOD:20070102T080000Z/PT2H,20070105T080000Z/PT2H
+RRULE:FREQ=WEEKLY
+END:VEVENT
+END:VCALENDAR
+""",
+ PyCalendarDateTime.parseText("20070103T000000Z"),
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20070101T000000Z
+DTSTAMP:20080601T120000Z
+RDATE;VALUE=PERIOD:20070102T080000Z/PT2H
+RRULE:FREQ=WEEKLY;UNTIL=20070102T235959Z
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for title, original, splitRID, result in data:
+ ical1 = Component.fromString(original.replace("\n", "\r\n"))
+ ical1.truncateRecurrenceDueToSplit(splitRID)
+ self.assertEqual(str(ical1), result.replace("\n", "\r\n"), "Failed comparison: %s" % (title))
+
def test_valid_recurrence(self):
data = (
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120825/fa9bdb11/attachment-0001.html>
More information about the calendarserver-changes
mailing list