[CalendarServer-changes] [3926] CalendarServer/branches/users/wsanchez/deployment/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Thu Mar 26 19:28:14 PDT 2009


Revision: 3926
          http://trac.macosforge.org/projects/calendarserver/changeset/3926
Author:   cdaboo at apple.com
Date:     2009-03-26 19:28:14 -0700 (Thu, 26 Mar 2009)
Log Message:
-----------
Support for open-ended time-range queries - merged from trunk.

Modified Paths:
--------------
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/caldavxml.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/dateops.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/ical.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/index.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/report_calquery.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/query/calendarquery.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/query/sqlgenerator.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_icalendar.py

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/caldavxml.py	2009-03-27 02:25:30 UTC (rev 3925)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/caldavxml.py	2009-03-27 02:28:14 UTC (rev 3926)
@@ -75,15 +75,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):
     """
@@ -915,9 +919,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)
@@ -959,6 +971,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.
@@ -1015,28 +1034,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} corrsponding 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.
@@ -1120,6 +1117,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.
@@ -1190,6 +1210,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.
@@ -1371,17 +1410,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
@@ -1407,7 +1446,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/branches/users/wsanchez/deployment/twistedcaldav/dateops.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/dateops.py	2009-03-27 02:25:30 UTC (rev 3925)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/dateops.py	2009-03-27 02:28:14 UTC (rev 3926)
@@ -182,18 +182,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/branches/users/wsanchez/deployment/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/ical.py	2009-03-27 02:25:30 UTC (rev 3925)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/ical.py	2009-03-27 02:28:14 UTC (rev 3926)
@@ -636,6 +636,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.
@@ -771,6 +786,36 @@
         instances.expandTimeRanges(componentSet, limit)
         return instances
 
+    def isRecurring(self):
+        """
+        Check whether any recurrence properties are present in any component.
+        """
+
+        # Extract appropriate sub-component if this is a VCALENDAR
+        if self.name() == "VCALENDAR":
+            for component in self.subcomponents():
+                if component.name() != "VTIMEZONE" and component.isRecurring():
+                    return True
+        else:
+            for propname in ("RRULE", "RDATE", "EXDATE", "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 resourceUID(self):
         """
         @return: the UID of the subcomponents in this component.

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/index.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/index.py	2009-03-27 02:25:30 UTC (rev 3925)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/index.py	2009-03-27 02:28:14 UTC (rev 3926)
@@ -81,7 +81,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.
     """
 
@@ -423,6 +423,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/branches/users/wsanchez/deployment/twistedcaldav/method/report_calquery.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/report_calquery.py	2009-03-27 02:25:30 UTC (rev 3925)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/report_calquery.py	2009-03-27 02:28:14 UTC (rev 3926)
@@ -25,16 +25,18 @@
 from twisted.internet.defer import deferredGenerator, succeed, waitForDeferred
 from twisted.web2 import responsecode
 from twisted.web2.dav import davxml
-from twisted.web2.dav.element.base import dav_namespace
+from twisted.web2.dav.element.base import dav_namespace, 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.instance import TooManyInstancesError
+from twistedcaldav.log import Logger
 from twistedcaldav.method import report_common
-from twistedcaldav.log import Logger
 
 log = Logger()
 
@@ -240,6 +242,12 @@
         d = waitForDeferred(report_common.applyToCalendarCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),)))
         yield d
         d.getResult()
+    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, (dav_namespace, "number-of-matches-within-limits")))

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/query/calendarquery.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/query/calendarquery.py	2009-03-27 02:25:30 UTC (rev 3925)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/query/calendarquery.py	2009-03-27 02:28:14 UTC (rev 3926)
@@ -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/branches/users/wsanchez/deployment/twistedcaldav/query/sqlgenerator.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/query/sqlgenerator.py	2009-03-27 02:25:30 UTC (rev 3925)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/query/sqlgenerator.py	2009-03-27 02:28:14 UTC (rev 3926)
@@ -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/branches/users/wsanchez/deployment/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_icalendar.py	2009-03-27 02:25:30 UTC (rev 3925)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/test/test_icalendar.py	2009-03-27 02:28:14 UTC (rev 3926)
@@ -238,3 +238,72 @@
 
         component = Component.fromString(data)
         self.assertEqual(component.getAttendeeProperties(("user3 at example.com",)), [])
+
+    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/20090326/b185bff8/attachment-0001.html>


More information about the calendarserver-changes mailing list