[CalendarServer-changes] [3861] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Sun Mar 15 11:47:32 PDT 2009
Revision: 3861
http://trac.macosforge.org/projects/calendarserver/changeset/3861
Author: cdaboo at apple.com
Date: 2009-03-15 11:47:31 -0700 (Sun, 15 Mar 2009)
Log Message:
-----------
Support for open-ended time-range queries. Fixes for both sqlite index queries and brute force
queries with open-ended (no end) special casing.
Modified Paths:
--------------
CalendarServer/trunk/run
CalendarServer/trunk/twistedcaldav/caldavxml.py
CalendarServer/trunk/twistedcaldav/dateops.py
CalendarServer/trunk/twistedcaldav/ical.py
CalendarServer/trunk/twistedcaldav/index.py
CalendarServer/trunk/twistedcaldav/method/report_calquery.py
CalendarServer/trunk/twistedcaldav/query/calendarquery.py
CalendarServer/trunk/twistedcaldav/query/sqlgenerator.py
CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/run 2009-03-15 18:47:31 UTC (rev 3861)
@@ -707,7 +707,7 @@
caldavtester="${top}/CalDAVTester";
-svn_get "CalDAVTester" "${caldavtester}" "${svn_uri_base}/CalDAVTester/trunk" 3856;
+svn_get "CalDAVTester" "${caldavtester}" "${svn_uri_base}/CalDAVTester/trunk" 3858;
#
# PyFlakes
Modified: CalendarServer/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py 2009-03-15 18:47:31 UTC (rev 3861)
@@ -1,5 +1,5 @@
##
-# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -83,15 +83,19 @@
CalDAV element containing a time range.
"""
allowed_attributes = {
- "start": True,
- "end" : True,
+ "start": False,
+ "end" : False,
}
def __init__(self, *children, **attributes):
super(CalDAVTimeRangeElement, self).__init__(*children, **attributes)
- self.start = parse_date_or_datetime(attributes["start"])
- self.end = parse_date_or_datetime(attributes["end" ])
+ # One of start or end must be present
+ if "start" not in attributes and "end" not in attributes:
+ raise ValueError("One of 'start' or 'end' must be present in CALDAV:time-range")
+
+ self.start = parse_date_or_datetime(attributes["start"]) if "start" in attributes else None
+ self.end = parse_date_or_datetime(attributes["end"]) if "end" in attributes else None
class CalDAVTimeZoneElement (CalDAVTextElement):
"""
@@ -929,9 +933,17 @@
# We need to prepare ourselves for a time-range query by pre-calculating
# the set of instances up to the latest time-range limit. That way we can
# avoid having to do some form of recurrence expansion for each query sub-part.
- maxend = self.children[0].getLastExpandTime()
+ maxend, isStartTime = self.getmaxtimerange()
if maxend:
- instances = component.expandTimeRanges(maxend)
+ if isStartTime:
+ if component.isRecurringUnbounded():
+ # Unbounded recurrence is always within a start-only time-range
+ instances = None
+ else:
+ # Expand the instances up to infinity
+ instances = component.expandTimeRanges(datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc))
+ else:
+ instances = component.expandTimeRanges(maxend)
else:
instances = None
self.children[0].setInstances(instances)
@@ -973,6 +985,13 @@
self.children[0].settzinfo(utc)
return utc
+ def getmaxtimerange(self):
+ """
+ Get the date farthest into the future in any time-range elements
+ """
+
+ return self.children[0].getmaxtimerange(None, False)
+
class ComponentFilter (CalDAVFilterElement):
"""
Limits a search to only the chosen component types.
@@ -1029,28 +1048,6 @@
return not self.defined
return self.defined
- def getLastExpandTime(self):
- """
- Get the latest time-range end value from any time-range element in this
- or child comp-filter elements.
- @return: the L{datetime.datetime} corresponding to the max. time to expand to,
- or None if there is no time-range
- """
-
- # Look for time-range in this filter
- if self.qualifier and self.defined:
- maxend = self.qualifier.end
- else:
- maxend = None
-
- # Now look at each comp-filter element in this one
- for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
- end = compfilter.getLastExpandTime()
- if end and ((maxend is None) or (end > maxend)):
- maxend = end
-
- return maxend
-
def setInstances(self, instances):
"""
Give the list of instances to each comp-filter element.
@@ -1134,6 +1131,29 @@
for x in self.filters:
x.settzinfo(tzinfo)
+ def getmaxtimerange(self, currentMaximum, currentIsStartTime):
+ """
+ Get the date furthest into the future in any time-range elements
+
+ @param currentMaximum: current future value to compare with
+ @type currentMaximum: L{datetime.datetime}
+ """
+
+ # Give tzinfo to any TimeRange we have
+ isStartTime = False
+ if isinstance(self.qualifier, TimeRange):
+ isStartTime = self.qualifier.end is None
+ compareWith = self.qualifier.start if isStartTime else self.qualifier.end
+ if currentMaximum is None or currentMaximum < compareWith:
+ currentMaximum = compareWith
+ currentIsStartTime = isStartTime
+
+ # Pass down to sub components/properties
+ for x in self.filters:
+ currentMaximum, currentIsStartTime = x.getmaxtimerange(currentMaximum, currentIsStartTime)
+
+ return currentMaximum, currentIsStartTime
+
class PropertyFilter (CalDAVFilterElement):
"""
Limits a search to specific properties.
@@ -1204,6 +1224,25 @@
if isinstance(self.qualifier, TimeRange):
self.qualifier.settzinfo(tzinfo)
+ def getmaxtimerange(self, currentMaximum, currentIsStartTime):
+ """
+ Get the date furthest into the future in any time-range elements
+
+ @param currentMaximum: current future value to compare with
+ @type currentMaximum: L{datetime.datetime}
+ """
+
+ # Give tzinfo to any TimeRange we have
+ isStartTime = False
+ if isinstance(self.qualifier, TimeRange):
+ isStartTime = self.qualifier.end is None
+ compareWith = self.qualifier.start if isStartTime else self.qualifier.end
+ if currentMaximum is None or currentMaximum < compareWith:
+ currentMaximum = compareWith
+ currentIsStartTime = isStartTime
+
+ return currentMaximum, currentIsStartTime
+
class ParameterFilter (CalDAVFilterElement):
"""
Limits a search to specific parameters.
@@ -1385,17 +1424,17 @@
@return: True if valid, False otherwise
"""
- if not isinstance(self.start, datetime.datetime):
+ if self.start is not None and not isinstance(self.start, datetime.datetime):
log.msg("start attribute in <time-range> is not a date-time: %s" % (self.start,))
return False
- if not isinstance(self.end, datetime.datetime):
+ if self.end is not None and not isinstance(self.end, datetime.datetime):
log.msg("end attribute in <time-range> is not a date-time: %s" % (self.end,))
return False
- if self.start.tzinfo != utc:
+ if self.start is not None and self.start.tzinfo != utc:
log.msg("start attribute in <time-range> is not UTC: %s" % (self.start,))
return False
- if self.end.tzinfo != utc:
- log.msg("end attribute in <time-range> is not UTC: %s" % (self.start,))
+ if self.end is not None and self.end.tzinfo != utc:
+ log.msg("end attribute in <time-range> is not UTC: %s" % (self.end,))
return False
# No other tests
@@ -1421,7 +1460,19 @@
if component is None:
return False
- assert instances is not None, "Failure to expand instance for time-range filter: %r" % (self,)
+ assert instances is not None or self.end is None, "Failure to expand instance for time-range filter: %r" % (self,)
+
+ # Special case open-ended unbounded
+ if instances is None:
+ if component.getRecurrenceIDUTC() is None:
+ return True
+ else:
+ # See if the overridden component's start is past the start
+ start, _ignore_end = component.getEffectiveStartEnd()
+ if start is None:
+ return True
+ else:
+ return start >= self.start
# Handle alarms as a special case
alarms = (component.name() == "VALARM")
Modified: CalendarServer/trunk/twistedcaldav/dateops.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/dateops.py 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/twistedcaldav/dateops.py 2009-03-15 18:47:31 UTC (rev 3861)
@@ -210,18 +210,25 @@
"""
# Can't compare datetime.date and datetime.datetime objects, so normalize
# to date if they are mixed.
- if isinstance(start1, datetime.datetime) and not isinstance(start2, datetime.datetime): start1 = start1.date()
- if isinstance(start2, datetime.datetime) and not isinstance(start1, datetime.datetime): start2 = start2.date()
+ if isinstance(start1, datetime.datetime) and (start2 is not None) and not isinstance(start2, datetime.datetime): start1 = start1.date()
+ if isinstance(start2, datetime.datetime) and (start1 is not None) and not isinstance(start1, datetime.datetime): start2 = start2.date()
if isinstance(end1, datetime.datetime) and (end2 is not None) and not isinstance(end2, datetime.datetime): end1 = end1.date()
if isinstance(end2, datetime.datetime) and (end1 is not None) and not isinstance(end1, datetime.datetime): end2 = end2.date()
# Note that start times are inclusive and end times are not.
- if end1 is not None and end2 is not None:
- return compareDateTime(start1, end2, defaulttz) < 0 and compareDateTime(end1, start2, defaulttz) > 0
- elif end1 is None:
- return compareDateTime(start1, start2, defaulttz) >= 0 and compareDateTime(start1, end2, defaulttz) < 0
- elif end2 is None:
- return compareDateTime(start2, start1, defaulttz) >= 0 and compareDateTime(start2, end1, defaulttz) < 0
+ if start1 is not None and start2 is not None:
+ if end1 is not None and end2 is not None:
+ return compareDateTime(start1, end2, defaulttz) < 0 and compareDateTime(end1, start2, defaulttz) > 0
+ elif end1 is None:
+ return compareDateTime(start1, start2, defaulttz) >= 0 and compareDateTime(start1, end2, defaulttz) < 0
+ elif end2 is None:
+ return compareDateTime(start2, end1, defaulttz) < 0
+ else:
+ return False
+ elif start1 is not None:
+ return compareDateTime(start1, end2, defaulttz) < 0
+ elif start2 is not None:
+ return compareDateTime(end1, end2, defaulttz) < 0 and compareDateTime(end1, start2, defaulttz) > 0
else:
return False
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2009-03-15 18:47:31 UTC (rev 3861)
@@ -34,14 +34,17 @@
from twisted.web2.dav.util import allDataFromStream
from twisted.web2.stream import IStream
+
from twistedcaldav.dateops import compareDateTime, normalizeToUTC, timeRangesOverlap,\
normalizeStartEndDuration, toString
-from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
from twistedcaldav.instance import InstanceList
from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
+
from vobject import newFromBehavior, readComponents
from vobject.base import Component as vComponent, ContentLine as vContentLine, ParseError as vParseError
from vobject.icalendar import TimezoneComponent, dateTimeToString, deltaToOffset, getTransition, stringToDate, stringToDateTime, stringToDurations, utc
+
import cStringIO as StringIO
import datetime
import heapq
@@ -725,6 +728,21 @@
self.transformAllToNative()
return self._vobject.getrruleset(addRDate)
+ def getEffectiveStartEnd(self):
+ # Get the start/end range needed for instance comparisons
+
+ if self.name() in ("VEVENT", "VJOURNAL",):
+ return self.getStartDateUTC(), self.getEndDateUTC()
+ elif self.name() == "VTODO":
+ start = self.getStartDateUTC()
+ due = self.getDueDateUTC()
+ if start is None and due is not None:
+ return due, due
+ else:
+ return start, due
+ else:
+ return None, None
+
def addProperty(self, property):
"""
Adds a property to this component.
@@ -921,11 +939,25 @@
if component.name() != "VTIMEZONE" and component.isRecurring():
return True
else:
- for propname in ("RRULE", "RDATE", "EXDATE", "RECUURENCE-ID",):
+ for propname in ("RRULE", "RDATE", "EXDATE", "RECURRENCE-ID",):
if self.hasProperty(propname):
return True
return False
+ def isRecurringUnbounded(self):
+ """
+ Check for unbounded recurrence.
+ """
+
+ master = self.masterComponent()
+ if master:
+ rrules = master.properties("RRULE")
+ for rrule in rrules:
+ s = str(rrule)
+ if "COUNT" not in s and "UNTIL" not in s:
+ return True
+ return False
+
def deriveInstance(self, rid, allowCancelled=False):
"""
Derive an instance from the master component that has the provided RECURRENCE-ID, but
Modified: CalendarServer/trunk/twistedcaldav/index.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/index.py 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/twistedcaldav/index.py 2009-03-15 18:47:31 UTC (rev 3861)
@@ -1,5 +1,5 @@
##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -34,7 +34,6 @@
import os
import time
import hashlib
-from dateutil.parser import parse as dateparse
try:
import sqlite3 as sqlite
@@ -86,7 +85,7 @@
class ReservationError(LookupError):
"""
- Attempt to reserve a UID which is already reserved or to unreverse a UID
+ Attempt to reserve a UID which is already reserved or to unreserve a UID
which is not reserved.
"""
@@ -281,16 +280,16 @@
if isinstance(filter, caldavxml.Filter):
qualifiers = calendarquery.sqlcalendarquery(filter)
if qualifiers is not None:
- if len(qualifiers[1]) > 1:
- # Bring index up to a given date if it's not already
- try:
- # TODO: is there a more reliable way to get the date?
- minDate = dateparse(qualifiers[1][1]).date()
- except ValueError:
- # this isn't a date after all
- minDate = None
- if minDate:
- self.testAndUpdateIndex(minDate)
+ # Determine how far we need to extend the current expansion of
+ # events. If we have an open-ended time-range we will expand one
+ # year past the start. That should catch bounded recurrences - unbounded
+ # will have been indexed with an "infinite" value always included.
+ maxDate, isStartDate = filter.getmaxtimerange()
+ if maxDate:
+ maxDate = maxDate.date()
+ if isStartDate:
+ maxDate += datetime.timedelta(days=365)
+ self.testAndUpdateIndex(maxDate)
else:
# We cannot handler this filter in an indexed search
raise IndexedSearchException()
@@ -487,7 +486,7 @@
instance = instances[key]
start = instance.start.replace(tzinfo=utc)
end = instance.end.replace(tzinfo=utc)
- float = ('N', 'Y')[instance.start.tzinfo is None]
+ float = 'Y' if instance.start.tzinfo is None else 'N'
self._db_execute(
"""
insert into TIMESPAN (NAME, FLOAT, START, END)
@@ -495,6 +494,19 @@
""", name, float, start, end
)
+ # Special - for unbounded recurrence we insert a value for "infinity"
+ # that will allow an open-ended time-range to always match it.
+ if calendar.isRecurringUnbounded():
+ start = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
+ end = datetime.datetime(2100, 1, 1, 1, 0, 0, tzinfo=utc)
+ float = 'N'
+ self._db_execute(
+ """
+ insert into TIMESPAN (NAME, FLOAT, START, END)
+ values (:1, :2, :3, :4)
+ """, name, float, start, end
+ )
+
self._db_execute(
"""
insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX)
Modified: CalendarServer/trunk/twistedcaldav/method/report_calquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_calquery.py 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/twistedcaldav/method/report_calquery.py 2009-03-15 18:47:31 UTC (rev 3861)
@@ -1,5 +1,5 @@
##
-# Copyright (c) 2006-2008 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -25,16 +25,19 @@
from twisted.internet.defer import succeed, inlineCallbacks, returnValue
from twisted.web2 import responsecode
from twisted.web2.dav import davxml
+from twisted.web2.dav.element.base import PCDATAElement
from twisted.web2.dav.http import ErrorResponse, MultiStatusResponse
from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
from twisted.web2.dav.util import joinURL
from twisted.web2.http import HTTPError, StatusResponse
-from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.caldavxml import caldav_namespace,\
+ NumberOfRecurrencesWithinLimits
from twistedcaldav.customxml import TwistedCalendarAccessProperty
-from twistedcaldav.method import report_common
from twistedcaldav.index import IndexedSearchException
+from twistedcaldav.instance import TooManyInstancesError
from twistedcaldav.log import Logger
+from twistedcaldav.method import report_common
log = Logger()
@@ -227,6 +230,12 @@
try:
depth = request.headers.getHeader("depth", "0")
yield report_common.applyToCalendarCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),))
+ except TooManyInstancesError, ex:
+ log.err("Too many instances need to be computed in calendar-query report")
+ raise HTTPError(ErrorResponse(
+ responsecode.FORBIDDEN,
+ NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
+ ))
except NumberOfMatchesWithinLimits:
log.err("Too many matching components in calendar-query report")
raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits()))
Modified: CalendarServer/trunk/twistedcaldav/query/calendarquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/calendarquery.py 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/twistedcaldav/query/calendarquery.py 2009-03-15 18:47:31 UTC (rev 3861)
@@ -1,5 +1,5 @@
##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -202,10 +202,15 @@
tzinfo = timerange.tzinfo
# Now force to floating UTC
- startfloat = floatoffset(start, tzinfo)
- endfloat = floatoffset(end, tzinfo)
+ startfloat = floatoffset(start, tzinfo) if start else None
+ endfloat = floatoffset(end, tzinfo) if end else None
- return str(start), str(end), str(startfloat), str(endfloat)
+ return (
+ str(start) if start else None,
+ str(end) if end else None,
+ str(startfloat) if startfloat else None,
+ str(endfloat) if endfloat else None,
+ )
def sqlcalendarquery(filter):
"""
Modified: CalendarServer/trunk/twistedcaldav/query/sqlgenerator.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/sqlgenerator.py 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/twistedcaldav/query/sqlgenerator.py 2009-03-15 18:47:31 UTC (rev 3861)
@@ -1,5 +1,5 @@
##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -44,7 +44,9 @@
INOP = " IN "
NOTINOP = " NOT IN "
- TIMESPANTEST = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s AND TIMESPAN.END > %s)) AND TIMESPAN.NAME == RESOURCE.NAME"
+ TIMESPANTEST = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s AND TIMESPAN.END > %s)) AND TIMESPAN.NAME == RESOURCE.NAME"
+ TIMESPANTEST_NOEND = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.END > %s)) AND TIMESPAN.NAME == RESOURCE.NAME"
+ TIMESPANTEST_NOSTART = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s)) AND TIMESPAN.NAME == RESOURCE.NAME"
def __init__(self, expr):
self.expression = expr
@@ -118,11 +120,20 @@
# time-range
elif isinstance(expr, expression.timerangeExpression):
- arg1 = self.setArgument(expr.end)
- arg2 = self.setArgument(expr.start)
- arg3 = self.setArgument(expr.endfloat)
- arg4 = self.setArgument(expr.startfloat)
- test = self.TIMESPANTEST % (arg1, arg2, arg3, arg4)
+ if expr.start and expr.end:
+ arg1 = self.setArgument(expr.end)
+ arg2 = self.setArgument(expr.start)
+ arg3 = self.setArgument(expr.endfloat)
+ arg4 = self.setArgument(expr.startfloat)
+ test = self.TIMESPANTEST % (arg1, arg2, arg3, arg4)
+ elif expr.start and expr.end is None:
+ arg1 = self.setArgument(expr.start)
+ arg2 = self.setArgument(expr.startfloat)
+ test = self.TIMESPANTEST_NOEND % (arg1, arg2)
+ elif not expr.start and expr.end:
+ arg1 = self.setArgument(expr.end)
+ arg2 = self.setArgument(expr.endfloat)
+ test = self.TIMESPANTEST_NOSTART % (arg1, arg2)
self.sout.write(test)
self.usedtimespan = True
Modified: CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_icalendar.py 2009-03-13 21:29:50 UTC (rev 3860)
+++ CalendarServer/trunk/twistedcaldav/test/test_icalendar.py 2009-03-15 18:47:31 UTC (rev 3861)
@@ -2166,3 +2166,72 @@
ical2 = str(ical2)
diff = "\n".join(unified_diff(ical1.split("\n"), ical2.split("\n")))
self.assertEqual(str(ical1), str(ical2), "Failed comparison: %s\n%s" % (title, diff,))
+
+ def test_recurring_unbounded(self):
+
+ data = (
+ (
+ "1.1 - non-recurring",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T000000Z
+DTEND:20090102T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ False
+ ),
+ (
+ "1.2 - recurring bounded COUNT",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T000000Z
+DTEND:20090102T000000Z
+RRULE:FREQ=DAILY;COUNT=2
+END:VEVENT
+END:VCALENDAR
+""",
+ False
+ ),
+ (
+ "1.3 - recurring bounded UNTIL",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T000000Z
+DTEND:20090102T000000Z
+RRULE:FREQ=DAILY;UNTIL=20090108T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ False
+ ),
+ (
+ "1.4 - recurring unbounded",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20090101T000000Z
+DTEND:20090102T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+ True
+ ),
+ )
+
+ for title, calendar, expected in data:
+ ical = Component.fromString(calendar)
+ result = ical.isRecurringUnbounded()
+ self.assertEqual(result, expected, "Failed recurring unbounded test: %s" % (title,))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090315/d114f4d8/attachment-0001.html>
More information about the calendarserver-changes
mailing list