[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