[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