[CalendarServer-changes] [4721] CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/ twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Mon Nov 9 07:42:58 PST 2009
Revision: 4721
http://trac.macosforge.org/projects/calendarserver/changeset/4721
Author: cdaboo at apple.com
Date: 2009-11-09 07:42:57 -0800 (Mon, 09 Nov 2009)
Log Message:
-----------
Move calendar-query logic out of caldavxml.
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/caldavxml.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/index.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/method/report_calquery.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/method/report_common.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/calendarquery.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/scheduling/processing.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_calendarquery.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_index.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_xml.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/queryfilter.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/__init__.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/test_calendarquery.py
CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/test_queryfilter.py
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/caldavxml.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/caldavxml.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -25,17 +25,12 @@
See draft spec: http://ietf.webdav.org/caldav/draft-dusseault-caldav.txt
"""
-import datetime
-
-from vobject.icalendar import utc, TimezoneComponent
-
from twisted.web2.dav import davxml
-
-from twistedcaldav.dateops import timeRangesOverlap
from twistedcaldav.ical import Component as iComponent
-from twistedcaldav.ical import Property as iProperty
from twistedcaldav.ical import parse_date_or_datetime
from twistedcaldav.log import Logger
+from vobject.icalendar import TimezoneComponent, utc
+import datetime
log = Logger()
@@ -97,41 +92,34 @@
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
+ def valid(self, level=0):
+ """
+ Indicate whether the time-range is valid (must be date-time in UTC).
+
+ @return: True if valid, False otherwise
+ """
+
+ 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 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 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 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
+ return True
+
class CalDAVTimeZoneElement (CalDAVTextElement):
"""
CalDAV element containing iCalendar data with a single VTIMEZONE component.
"""
- def __init__(self, *children, **attributes):
- super(CalDAVTimeZoneElement, self).__init__(*children, **attributes)
- # An error in the data needs to be reported as a pre-condition error rather than
- # an XML parse error. So this test is moved out of here into a separate method that
- # gets called and can cause the proper WebDAV DAV:error response.
-
- # TODO: Remove the comment above and commented code below once this has been properly tested.
-#
-# try:
-# calendar = self.calendar()
-# if calendar is None: raise ValueError("No data")
-# except ValueError, e:
-# log.err("Invalid iCalendar data (%s): %r" % (calendar, e))
-# raise
-#
-# found = False
-#
-# for subcomponent in calendar.subcomponents():
-# if subcomponent.name() == "VTIMEZONE":
-# if found:
-# raise ValueError("CalDAV:%s may not contain iCalendar data with more than one VTIMEZONE component" % (self.name,))
-# else:
-# found = True
-# else:
-# # FIXME: Spec doesn't seem to really disallow this; it's unclear...
-# raise ValueError("%s component not allowed in CalDAV:timezone data" % (subcomponent.name(),))
-#
-# if not found:
-# raise ValueError("CalDAV:%s must contain iCalendar data with a VTIMEZONE component" % (self.name,))
-
def calendar(self):
"""
Returns a calendar component derived from this element, which contains
@@ -169,64 +157,6 @@
return found
-class CalDAVFilterElement (CalDAVElement):
- """
- CalDAV filter element.
- """
- def __init__(self, *children, **attributes):
- # FIXME: is-defined is obsoleted by CalDAV-access-09. Filter it out here for compatibility.
- children = [c for c in children if c is not None and c.qname() != (caldav_namespace, "is-defined")]
-
- super(CalDAVFilterElement, self).__init__(*children, **attributes)
-
- qualifier = None
- filters = []
-
- for child in self.children:
- qname = child.qname()
-
- if qname in (
- (caldav_namespace, "is-not-defined"),
- (caldav_namespace, "time-range"),
- (caldav_namespace, "text-match"),
- ):
- if qualifier is not None:
- raise ValueError("Only one of CalDAV:time-range, CalDAV:text-match allowed")
- qualifier = child
-
- else:
- filters.append(child)
-
- if qualifier and (qualifier.qname() == (caldav_namespace, "is-not-defined")) and (len(filters) != 0):
- raise ValueError("No other tests allowed when CalDAV:is-not-defined is present")
-
- self.qualifier = qualifier
- self.filters = filters
- self.filter_name = attributes["name"]
- if isinstance(self.filter_name, unicode):
- self.filter_name = self.filter_name.encode("utf-8")
- self.defined = not self.qualifier or (self.qualifier.qname() != (caldav_namespace, "is-not-defined"))
-
- def match(self, item, access=None):
- """
- Returns True if the given calendar item (either a component, property or parameter value)
- matches this filter, False otherwise.
- """
-
- # Always return True for the is-not-defined case as the result of this will
- # be negated by the caller
- if not self.defined: return True
-
- if self.qualifier and not self.qualifier.match(item, access): return False
-
- if len(self.filters) > 0:
- for filter in self.filters:
- if filter._match(item, access):
- return True
- return False
- else:
- return True
-
class CalendarHomeSet (CalDAVElement):
"""
The calendar collections URLs for this principal's calendar user.
@@ -341,7 +271,7 @@
def __init__(self, *children, **attributes):
super(CalendarQuery, self).__init__(*children, **attributes)
- query = None
+ props = None
filter = None
timezone = None
@@ -353,9 +283,9 @@
(davxml.dav_namespace, "propname"),
(davxml.dav_namespace, "prop" ),
):
- if query is not None:
+ if props is not None:
raise ValueError("Only one of CalDAV:allprop, CalDAV:propname, CalDAV:prop allowed")
- query = child
+ props = child
elif qname == (caldav_namespace, "filter"):
filter = child
@@ -370,7 +300,7 @@
if filter is None:
raise ValueError("CALDAV:filter required")
- self.query = query
+ self.props = props
self.filter = filter
self.timezone = timezone
@@ -689,79 +619,7 @@
allowed_children = { (caldav_namespace, "comp-filter"): (1, 1) }
- def match(self, component, access=None):
- """
- Returns True if the given calendar component matches this filter, False
- otherwise.
- """
-
- # We only care about certain access restrictions.
- if access not in (iComponent.ACCESS_CONFIDENTIAL, iComponent.ACCESS_RESTRICTED):
- access = None
-
- # 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, isStartTime = self.getmaxtimerange()
- if 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), ignoreInvalidInstances=True)
- else:
- instances = component.expandTimeRanges(maxend, ignoreInvalidInstances=True)
- else:
- instances = None
- self.children[0].setInstances(instances)
-
- # <filter> contains exactly one <comp-filter>
- return self.children[0].match(component, access)
-
- def valid(self):
- """
- Indicate whether this filter element's structure is valid wrt iCalendar
- data object model.
-
- @return: True if valid, False otherwise
- """
-
- # Must have one child element for VCALENDAR
- return self.children[0].valid(0)
-
- def settimezone(self, tzelement):
- """
- Set the default timezone to use with this query.
- @param calendar: a L{Component} for the VCALENDAR containing the one
- VTIMEZONE that we want
- @return: the L{datetime.tzinfo} derived from the VTIMEZONE or utc.
- """
- assert tzelement is None or isinstance(tzelement, CalDAVTimeZoneElement)
-
- if tzelement is not None:
- calendar = tzelement.calendar()
- if calendar is not None:
- for subcomponent in calendar.subcomponents():
- if subcomponent.name() == "VTIMEZONE":
- # <filter> contains exactly one <comp-filter>
- tzinfo = subcomponent.gettzinfo()
- self.children[0].settzinfo(tzinfo)
- return tzinfo
-
- # Default to using utc tzinfo
- 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):
+class ComponentFilter (CalDAVElement):
"""
Limits a search to only the chosen component types.
(CalDAV-access-09, section 9.6.1)
@@ -769,7 +627,6 @@
name = "comp-filter"
allowed_children = {
- (caldav_namespace, "is-defined" ): (0, 1), # FIXME: obsoleted in CalDAV-access-09
(caldav_namespace, "is-not-defined" ): (0, 1),
(caldav_namespace, "time-range" ): (0, 1),
(caldav_namespace, "comp-filter" ): (0, None),
@@ -777,153 +634,7 @@
}
allowed_attributes = { "name": True }
- def match(self, item, access):
- """
- Returns True if the given calendar item (which is a component)
- matches this filter, False otherwise.
- This specialization uses the instance matching option of the time-range filter
- to minimize instance expansion.
- """
-
- # Always return True for the is-not-defined case as the result of this will
- # be negated by the caller
- if not self.defined: return True
-
- if self.qualifier and not self.qualifier.matchinstance(item, self.instances): return False
-
- if len(self.filters) > 0:
- for filter in self.filters:
- if filter._match(item, access):
- return True
- return False
- else:
- return True
-
- def _match(self, component, access):
- # At least one subcomponent must match (or is-not-defined is set)
- for subcomponent in component.subcomponents():
- # If access restrictions are in force, restrict matching to specific components only.
- # In particular do not match VALARM.
- if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",):
- continue
-
- # Try to match the component name
- if isinstance(self.filter_name, str):
- if subcomponent.name() != self.filter_name: continue
- else:
- if subcomponent.name() not in self.filter_name: continue
- if self.match(subcomponent, access): break
- else:
- return not self.defined
- return self.defined
-
- def setInstances(self, instances):
- """
- Give the list of instances to each comp-filter element.
- @param instances: the list of instances.
- """
- self.instances = instances
- for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
- compfilter.setInstances(instances)
-
- def valid(self, level):
- """
- Indicate whether this filter element's structure is valid wrt iCalendar
- data object model.
-
- @param level: the nesting level of this filter element, 0 being the top comp-filter.
- @return: True if valid, False otherwise
- """
-
- # Check for time-range
- timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
-
- if level == 0:
- # Must have VCALENDAR at the top
- if (self.filter_name != "VCALENDAR") or timerange:
- log.msg("Top-level comp-filter must be VCALENDAR, instead: %s" % (self.filter_name,))
- return False
- elif level == 1:
- # Disallow VCALENDAR, VALARM, STANDARD, DAYLIGHT, AVAILABLE at the top, everything else is OK
- if self.filter_name in ("VCALENDAR", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE"):
- log.msg("comp-filter wrong component type: %s" % (self.filter_name,))
- return False
-
- # time-range only on VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY
- if timerange and self.filter_name not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY"):
- log.msg("time-range cannot be used with component %s" % (self.filter_name,))
- return False
- elif level == 2:
- # Disallow VCALENDAR, VTIMEZONE, VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY at the top, everything else is OK
- if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY")):
- log.msg("comp-filter wrong sub-component type: %s" % (self.filter_name,))
- return False
-
- # time-range only on VALARM, AVAILABLE
- if timerange and self.filter_name not in ("VALARM", "AVAILABLE",):
- log.msg("time-range cannot be used with sub-component %s" % (self.filter_name,))
- return False
- else:
- # Disallow all standard iCal components anywhere else
- if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE")) or timerange:
- log.msg("comp-filter wrong standard component type: %s" % (self.filter_name,))
- return False
-
- # Test each property
- for propfilter in [x for x in self.filters if isinstance(x, PropertyFilter)]:
- if not propfilter.valid():
- return False
-
- # Test each component
- for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
- if not compfilter.valid(level + 1):
- return False
-
- # Test the time-range
- if timerange:
- if not self.qualifier.valid():
- return False
-
- return True
-
- def settzinfo(self, tzinfo):
- """
- Set the default timezone to use with this query.
- @param tzinfo: a L{datetime.tzinfo} to use.
- """
-
- # Give tzinfo to any TimeRange we have
- if isinstance(self.qualifier, TimeRange):
- self.qualifier.settzinfo(tzinfo)
-
- # Pass down to sub components/properties
- 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):
+class PropertyFilter (CalDAVElement):
"""
Limits a search to specific properties.
(CalDAV-access-09, section 9.6.2)
@@ -931,7 +642,6 @@
name = "prop-filter"
allowed_children = {
- (caldav_namespace, "is-defined" ): (0, 1), # FIXME: obsoleted in CalDAV-access-09
(caldav_namespace, "is-not-defined" ): (0, 1),
(caldav_namespace, "time-range" ): (0, 1),
(caldav_namespace, "text-match" ): (0, 1),
@@ -939,80 +649,7 @@
}
allowed_attributes = { "name": True }
- def _match(self, component, access):
- # When access restriction is in force, we need to only allow matches against the properties
- # allowed by the access restriction level.
- if access:
- allowedProperties = iComponent.confidentialPropertiesMap.get(component.name(), None)
- if allowedProperties and access == iComponent.ACCESS_RESTRICTED:
- allowedProperties += iComponent.extraRestrictedProperties
- else:
- allowedProperties = None
-
- # At least one property must match (or is-not-defined is set)
- for property in component.properties():
- # Apply access restrictions, if any.
- if allowedProperties is not None and property.name() not in allowedProperties:
- continue
- if property.name() == self.filter_name and self.match(property, access): break
- else:
- return not self.defined
- return self.defined
-
- def valid(self):
- """
- Indicate whether this filter element's structure is valid wrt iCalendar
- data object model.
-
- @return: True if valid, False otherwise
- """
-
- # Check for time-range
- timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
-
- # time-range only on COMPLETED, CREATED, DTSTAMP, LAST-MODIFIED
- if timerange and self.filter_name not in ("COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
- log.msg("time-range cannot be used with property %s" % (self.filter_name,))
- return False
-
- # Test the time-range
- if timerange:
- if not self.qualifier.valid():
- return False
-
- # No other tests
- return True
-
- def settzinfo(self, tzinfo):
- """
- Set the default timezone to use with this query.
- @param tzinfo: a L{datetime.tzinfo} to use.
- """
-
- # Give tzinfo to any TimeRange we have
- 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):
+class ParameterFilter (CalDAVElement):
"""
Limits a search to specific parameters.
(CalDAV-access-09, section 9.6.3)
@@ -1020,42 +657,11 @@
name = "param-filter"
allowed_children = {
- (caldav_namespace, "is-defined" ): (0, 1), # FIXME: obsoleted in CalDAV-access-09
(caldav_namespace, "is-not-defined" ): (0, 1),
(caldav_namespace, "text-match" ): (0, 1),
}
allowed_attributes = { "name": True }
- def _match(self, property, access):
- # We have to deal with the problem that the 'Native' form of a property
- # will be missing the TZID parameter due to the conversion performed. Converting
- # to non-native for the entire calendar object causes problems elsewhere, so its
- # best to do it here for this one special case.
- if self.filter_name == "TZID":
- transformed = property.transformAllFromNative()
- else:
- transformed = False
-
- # At least one property must match (or is-not-defined is set)
- result = not self.defined
- for parameterName in property.params().keys():
- if parameterName == self.filter_name and self.match(property.params()[parameterName], access):
- result = self.defined
- break
-
- if transformed:
- property.transformAllToNative()
- return result
-
-class IsDefined (CalDAVEmptyElement):
- """
- FIXME: Removed from spec.
- """
- name = "is-defined"
-
- def match(self, component, access):
- return component is not None
-
class IsNotDefined (CalDAVEmptyElement):
"""
Specifies that the named iCalendar item does not exist.
@@ -1063,13 +669,6 @@
"""
name = "is-not-defined"
- def match(self, component, access):
- # Oddly, this needs always to return True so that it appears there is
- # a match - but we then "negate" the result if is-not-defined is set.
- # Actually this method should never be called as we special case the
- # is-not-defined option.
- return True
-
class TextMatch (CalDAVTextElement):
"""
Specifies a substring match on a property or parameter value.
@@ -1097,68 +696,6 @@
"negate-condition": False
}
- def __init__(self, *children, **attributes):
- super(TextMatch, self).__init__(*children, **attributes)
-
- if "caseless" in attributes:
- caseless = attributes["caseless"]
- if caseless == "yes":
- self.caseless = True
- elif caseless == "no":
- self.caseless = False
- else:
- self.caseless = True
-
- if "negate-condition" in attributes:
- negate = attributes["negate-condition"]
- if negate == "yes":
- self.negate = True
- elif caseless == "no":
- self.negate = False
- else:
- self.negate = False
-
- def match(self, item, access):
- """
- Match the text for the item.
- If the item is a property, then match the property value,
- otherwise it may be a list of parameter values - try to match anyone of those
- """
- if item is None: return False
-
- if isinstance(item, iProperty):
- values = [item.value()]
- else:
- values = item
-
- test = unicode(str(self), "utf-8")
- if self.caseless:
- test = test.lower()
-
- def _textCompare(s):
- if self.caseless:
- if s.lower().find(test) != -1:
- return True, not self.negate
- else:
- if s.find(test) != -1:
- return True, not self.negate
- return False, False
-
- for value in values:
- # NB Its possible that we have a text list value which appears as a Python list,
- # so we need to check for that an iterate over the list.
- if isinstance(value, list):
- for subvalue in value:
- matched, result = _textCompare(subvalue)
- if matched:
- return result
- else:
- matched, result = _textCompare(value)
- if matched:
- return result
-
- return self.negate
-
class TimeZone (CalDAVTimeZoneElement):
"""
Specifies a time zone component.
@@ -1173,103 +710,6 @@
"""
name = "time-range"
- def __init__(self, *children, **attributes):
- super(TimeRange, self).__init__(*children, **attributes)
- self.tzinfo = None
-
- def settzinfo(self, tzinfo):
- """
- Set the default timezone to use with this query.
- @param tzinfo: a L{datetime.tzinfo} to use.
- """
-
- # Give tzinfo to any TimeRange we have
- self.tzinfo = tzinfo
-
- def valid(self):
- """
- Indicate whether the time-range is valid (must be date-time in UTC).
-
- @return: True if valid, False otherwise
- """
-
- 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 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 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 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
- return True
-
- def match(self, property, access):
- """
- NB This is only called when doing a time-range match on a property.
- """
- if property is None:
- return False
- else:
- return property.containsTimeRange(self.start, self.end, self.tzinfo)
-
- def matchinstance(self, component, instances):
- """
- Test whether this time-range element causes a match to the specified component
- using the specified set of instances to determine the expanded time ranges.
- @param component: the L{Component} to test.
- @param instances: the list of expanded instances.
- @return: True if the time-range query matches, False otherwise.
- """
- if component is None:
- return False
-
- 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")
- if alarms:
- testcomponent = component._parent
- else:
- testcomponent = component
-
- for key in instances:
- instance = instances[key]
-
- # First make sure components match
- if not testcomponent.same(instance.component):
- continue
-
- if alarms:
- # Get all the alarm triggers for this instance and test each one
- triggers = instance.getAlarmTriggers()
- for trigger in triggers:
- if timeRangesOverlap(trigger, None, self.start, self.end, self.tzinfo):
- return True
- else:
- # Regular instance overlap test
- if timeRangesOverlap(instance.start, instance.end, self.start, self.end, self.tzinfo):
- return True
-
- return False
-
class CalendarMultiGet (CalDAVElement):
"""
CalDAV report used to retrieve specific calendar component items via their
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/index.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/index.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/index.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -46,9 +46,9 @@
from twistedcaldav.ical import Component
from twistedcaldav.query import calendarquery
+from twistedcaldav.query import queryfilter
from twistedcaldav.sql import AbstractSQLDatabase
from twistedcaldav.sql import db_prefix
-from twistedcaldav import caldavxml
from twistedcaldav.instance import InvalidOverriddenInstanceError
from twistedcaldav.log import Logger, LoggingMixIn
from twistedcaldav.config import config
@@ -286,7 +286,7 @@
# Make sure we have a proper Filter element and get the partial SQL
# statement to use.
- if isinstance(filter, caldavxml.Filter):
+ if isinstance(filter, queryfilter.Filter):
qualifiers = calendarquery.sqlcalendarquery(filter)
if qualifiers is not None:
# Determine how far we need to extend the current expansion of
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/method/report_calquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/method/report_calquery.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/method/report_calquery.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -40,6 +40,7 @@
from twistedcaldav.instance import TooManyInstancesError
from twistedcaldav.log import Logger
from twistedcaldav.method import report_common
+from twistedcaldav.query import queryfilter
log = Logger()
@@ -64,10 +65,11 @@
responses = []
- filter = calendar_query.filter
- query = calendar_query.query
+ xmlfilter = calendar_query.filter
+ filter = queryfilter.Filter(xmlfilter)
+ props = calendar_query.props
- assert query is not None
+ assert props is not None
# Get the original timezone provided in the query, if any, and validate it now
query_timezone = None
@@ -80,19 +82,19 @@
filter.settimezone(query_tz)
query_timezone = tuple(calendar_query.timezone.calendar().subcomponents())[0]
- if query.qname() == ("DAV:", "allprop"):
+ if props.qname() == ("DAV:", "allprop"):
propertiesForResource = report_common.allPropertiesForResource
generate_calendar_data = False
- elif query.qname() == ("DAV:", "propname"):
+ elif props.qname() == ("DAV:", "propname"):
propertiesForResource = report_common.propertyNamesForResource
generate_calendar_data = False
- elif query.qname() == ("DAV:", "prop"):
+ elif props.qname() == ("DAV:", "prop"):
propertiesForResource = report_common.propertyListForResource
# Verify that any calendar-data element matches what we can handle
- result, message, generate_calendar_data = report_common.validPropertyListCalendarDataTypeVersion(query)
+ result, message, generate_calendar_data = report_common.validPropertyListCalendarDataTypeVersion(props)
if not result:
log.err(message)
raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
@@ -102,7 +104,7 @@
# Verify that the filter element is valid
if (filter is None) or not filter.valid():
- log.err("Invalid filter element: %r" % (filter,))
+ log.err("Invalid filter element: %r" % (xmlfilter,))
raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-filter")))
matchcount = [0]
@@ -145,7 +147,7 @@
else:
href = davxml.HRef.fromString(uri)
- return report_common.responseForHref(request, responses, href, resource, calendar, timezone, propertiesForResource, query, isowner)
+ return report_common.responseForHref(request, responses, href, resource, calendar, timezone, propertiesForResource, props, isowner)
else:
return succeed(None)
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/method/report_common.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/method/report_common.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -60,6 +60,7 @@
from twistedcaldav.instance import InstanceList
from twistedcaldav.index import IndexedSearchException
from twistedcaldav.log import Logger
+from twistedcaldav.query import queryfilter
log = Logger()
@@ -335,6 +336,7 @@
name="VCALENDAR",
)
)
+ filter = queryfilter.Filter(filter)
# Get the timezone property from the collection, and store in the query filter
# for use during the query itself.
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/calendarquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/calendarquery.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/calendarquery.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -27,9 +27,7 @@
]
from twistedcaldav.dateops import floatoffset
-from twistedcaldav.query import sqlgenerator
-from twistedcaldav.query import expression
-from twistedcaldav import caldavxml
+from twistedcaldav.query import expression, sqlgenerator, queryfilter
# SQL Index column (field) names
@@ -51,17 +49,17 @@
# Lets assume we have a valid filter from the outset.
# Top-level filter contains exactly one comp-filter element
- assert len(filter.children) == 1
- vcalfilter = filter.children[0]
- assert isinstance(vcalfilter, caldavxml.ComponentFilter)
+ assert filter.child is not None
+ vcalfilter = filter.child
+ assert isinstance(vcalfilter, queryfilter.ComponentFilter)
assert vcalfilter.filter_name == "VCALENDAR"
- if len(vcalfilter.children) > 0:
+ if len(vcalfilter.filters) > 0:
# Only comp-filters are handled
- for _ignore in [x for x in vcalfilter.children if not isinstance(x, caldavxml.ComponentFilter)]:
+ for _ignore in [x for x in vcalfilter.filters if not isinstance(x, queryfilter.ComponentFilter)]:
raise ValueError
- return compfilterListExpression(vcalfilter.children)
+ return compfilterListExpression(vcalfilter.filters)
else:
return expression.allExpression()
@@ -98,13 +96,13 @@
expressions.append(expression.inExpression(FIELD_TYPE, compfilter.filter_name, True))
# Handle time-range
- if compfilter.qualifier and isinstance(compfilter.qualifier, caldavxml.TimeRange):
+ if compfilter.qualifier and isinstance(compfilter.qualifier, queryfilter.TimeRange):
start, end, startfloat, endfloat = getTimerangeArguments(compfilter.qualifier)
expressions.append(expression.timerangeExpression(start, end, startfloat, endfloat))
# Handle properties - we can only do UID right now
props = []
- for p in [x for x in compfilter.filters if isinstance(x, caldavxml.PropertyFilter)]:
+ for p in [x for x in compfilter.filters if isinstance(x, queryfilter.PropertyFilter)]:
props.append(propfilterExpression(p))
if len(props) > 1:
propsExpression = expression.orExpression[props]
@@ -115,7 +113,7 @@
# Handle embedded components - we do not right now as our Index does not handle them
comps = []
- for _ignore in [x for x in compfilter.filters if isinstance(x, caldavxml.ComponentFilter)]:
+ for _ignore in [x for x in compfilter.filters if isinstance(x, queryfilter.ComponentFilter)]:
raise ValueError
if len(comps) > 1:
compsExpression = expression.orExpression[comps]
@@ -153,16 +151,16 @@
return expression.isExpression(FIELD_UID, "", True)
# Handle time-range - we cannot do this with our Index right now
- if propfilter.qualifier and isinstance(propfilter.qualifier, caldavxml.TimeRange):
+ if propfilter.qualifier and isinstance(propfilter.qualifier, queryfilter.TimeRange):
raise ValueError
# Handle text-match
tm = None
- if propfilter.qualifier and isinstance(propfilter.qualifier, caldavxml.TextMatch):
+ if propfilter.qualifier and isinstance(propfilter.qualifier, queryfilter.TextMatch):
if propfilter.qualifier.negate:
- tm = expression.notcontainsExpression(propfilter.filter_name, str(propfilter.qualifier), propfilter.qualifier)
+ tm = expression.notcontainsExpression(propfilter.filter_name, propfilter.qualifier.text, propfilter.qualifier.caseless)
else:
- tm = expression.containsExpression(propfilter.filter_name, str(propfilter.qualifier), propfilter.qualifier)
+ tm = expression.containsExpression(propfilter.filter_name, propfilter.qualifier.text, propfilter.qualifier.caseless)
# Handle embedded parameters - we do not right now as our Index does not handle them
params = []
@@ -227,79 +225,3 @@
return sql.generate()
except ValueError:
return None
-
-
-if __name__ == "__main__":
- import datetime
-
- filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- *[caldavxml.ComponentFilter(
- *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
- **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
- )],
- **{"name":"VCALENDAR"}
- )
- )
-
- # A complete implementation of current DST rules for major US time zones.
-
- def first_sunday_on_or_after(dt):
- days_to_go = 6 - dt.weekday()
- if days_to_go:
- dt += datetime.timedelta(days_to_go)
- return dt
-
- # In the US, DST starts at 2am (standard time) on the first Sunday in April.
- DSTSTART = datetime.datetime(1, 4, 1, 2)
- # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
- # which is the first Sunday on or after Oct 25.
- DSTEND = datetime.datetime(1, 10, 25, 1)
-
- ZERO = datetime.timedelta(0)
- HOUR = datetime.timedelta(hours=1)
-
- class USTimeZone(datetime.tzinfo):
-
- def __init__(self, hours, reprname, stdname, dstname):
- self.stdoffset = datetime.timedelta(hours=hours)
- self.reprname = reprname
- self.stdname = stdname
- self.dstname = dstname
-
- def __repr__(self):
- return self.reprname
-
- def tzname(self, dt):
- if self.dst(dt):
- return self.dstname
- else:
- return self.stdname
-
- def utcoffset(self, dt):
- return self.stdoffset + self.dst(dt)
-
- def dst(self, dt):
- if dt is None or dt.tzinfo is None:
- # An exception may be sensible here, in one or both cases.
- # It depends on how you want to treat them. The default
- # fromutc() implementation (called by the default astimezone()
- # implementation) passes a datetime with dt.tzinfo is self.
- return ZERO
- assert dt.tzinfo is self
-
- # Find first Sunday in April & the last in October.
- start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
- end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
-
- # Can't compare naive to aware objects, so strip the timezone from
- # dt first.
- if start <= dt.replace(tzinfo=None) < end:
- return HOUR
- else:
- return ZERO
-
- Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
- filter.children[0].settzinfo(Eastern)
-
- print sqlcalendarquery(filter)
Added: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/queryfilter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/queryfilter.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/queryfilter.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -0,0 +1,649 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Object model of CALDAV:filter element used in a calendar-query.
+"""
+
+__all__ = [
+ "Filter",
+]
+
+from twistedcaldav.caldavxml import caldav_namespace, CalDAVTimeZoneElement
+from twistedcaldav.dateops import timeRangesOverlap
+from twistedcaldav.ical import Component, Property, parse_date_or_datetime
+from twistedcaldav.log import Logger
+from vobject.icalendar import utc
+import datetime
+
+log = Logger()
+
+class FilterBase(object):
+ """
+ Determines which matching components are returned.
+ """
+
+ def __init__(self, xml_element):
+ self.xmlelement = xml_element
+
+ def match(self, item, access=None):
+ raise NotImplementedError
+
+ def valid(self, level=0):
+ raise NotImplementedError
+
+class Filter(FilterBase):
+ """
+ Determines which matching components are returned.
+ """
+
+ def __init__(self, xml_element):
+
+ super(Filter, self).__init__(xml_element)
+
+ # One comp-filter element must be present
+ if len(xml_element.children) != 1 or xml_element.children[0].qname() != (caldav_namespace, "comp-filter"):
+ raise ValueError("Invalid CALDAV:filter element: %s" % (xml_element,))
+
+ self.child = ComponentFilter(xml_element.children[0])
+
+ def match(self, component, access=None):
+ """
+ Returns True if the given calendar component matches this filter, False
+ otherwise.
+ """
+
+ # We only care about certain access restrictions.
+ if access not in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
+ access = None
+
+ # 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, isStartTime = self.getmaxtimerange()
+ if 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), ignoreInvalidInstances=True)
+ else:
+ instances = component.expandTimeRanges(maxend, ignoreInvalidInstances=True)
+ else:
+ instances = None
+ self.child.setInstances(instances)
+
+ # <filter> contains exactly one <comp-filter>
+ return self.child.match(component, access)
+
+ def valid(self):
+ """
+ Indicate whether this filter element's structure is valid wrt iCalendar
+ data object model.
+
+ @return: True if valid, False otherwise
+ """
+
+ # Must have one child element for VCALENDAR
+ return self.child.valid(0)
+
+ def settimezone(self, tzelement):
+ """
+ Set the default timezone to use with this query.
+ @param calendar: a L{Component} for the VCALENDAR containing the one
+ VTIMEZONE that we want
+ @return: the L{datetime.tzinfo} derived from the VTIMEZONE or utc.
+ """
+ assert tzelement is None or isinstance(tzelement, CalDAVTimeZoneElement)
+
+ if tzelement is not None:
+ calendar = tzelement.calendar()
+ if calendar is not None:
+ for subcomponent in calendar.subcomponents():
+ if subcomponent.name() == "VTIMEZONE":
+ # <filter> contains exactly one <comp-filter>
+ tzinfo = subcomponent.gettzinfo()
+ self.child.settzinfo(tzinfo)
+ return tzinfo
+
+ # Default to using utc tzinfo
+ self.child.settzinfo(utc)
+ return utc
+
+ def getmaxtimerange(self):
+ """
+ Get the date farthest into the future in any time-range elements
+ """
+
+ return self.child.getmaxtimerange(None, False)
+
+class FilterChildBase(FilterBase):
+ """
+ CalDAV filter element.
+ """
+
+ def __init__(self, xml_element):
+
+ super(FilterChildBase, self).__init__(xml_element)
+
+ qualifier = None
+ filters = []
+
+ for child in xml_element.children:
+ qname = child.qname()
+
+ if qname in (
+ (caldav_namespace, "is-not-defined"),
+ (caldav_namespace, "time-range"),
+ (caldav_namespace, "text-match"),
+ ):
+ if qualifier is not None:
+ raise ValueError("Only one of CalDAV:time-range, CalDAV:text-match allowed")
+
+ if qname == (caldav_namespace, "is-not-defined"):
+ qualifier = IsNotDefined(child)
+ elif qname == (caldav_namespace, "time-range"):
+ qualifier = TimeRange(child)
+ elif qname == (caldav_namespace, "text-match"):
+ qualifier = TextMatch(child)
+
+ elif qname == (caldav_namespace, "comp-filter"):
+ filters.append(ComponentFilter(child))
+ elif qname == (caldav_namespace, "prop-filter"):
+ filters.append(PropertyFilter(child))
+ elif qname == (caldav_namespace, "param-filter"):
+ filters.append(ParameterFilter(child))
+ else:
+ raise ValueError("Unknown child element: %s" % (qname,))
+
+ if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
+ raise ValueError("No other tests allowed when CalDAV:is-not-defined is present")
+
+ self.qualifier = qualifier
+ self.filters = filters
+ self.filter_name = xml_element.attributes["name"]
+ if isinstance(self.filter_name, unicode):
+ self.filter_name = self.filter_name.encode("utf-8")
+ self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
+
+ def match(self, item, access=None):
+ """
+ Returns True if the given calendar item (either a component, property or parameter value)
+ matches this filter, False otherwise.
+ """
+
+ # Always return True for the is-not-defined case as the result of this will
+ # be negated by the caller
+ if not self.defined: return True
+
+ if self.qualifier and not self.qualifier.match(item, access): return False
+
+ if len(self.filters) > 0:
+ for filter in self.filters:
+ if filter._match(item, access):
+ return True
+ return False
+ else:
+ return True
+
+class ComponentFilter (FilterChildBase):
+ """
+ Limits a search to only the chosen component types.
+ """
+
+ def match(self, item, access):
+ """
+ Returns True if the given calendar item (which is a component)
+ matches this filter, False otherwise.
+ This specialization uses the instance matching option of the time-range filter
+ to minimize instance expansion.
+ """
+
+ # Always return True for the is-not-defined case as the result of this will
+ # be negated by the caller
+ if not self.defined: return True
+
+ if self.qualifier and not self.qualifier.matchinstance(item, self.instances): return False
+
+ if len(self.filters) > 0:
+ for filter in self.filters:
+ if filter._match(item, access):
+ return True
+ return False
+ else:
+ return True
+
+ def _match(self, component, access):
+ # At least one subcomponent must match (or is-not-defined is set)
+ for subcomponent in component.subcomponents():
+ # If access restrictions are in force, restrict matching to specific components only.
+ # In particular do not match VALARM.
+ if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",):
+ continue
+
+ # Try to match the component name
+ if isinstance(self.filter_name, str):
+ if subcomponent.name() != self.filter_name: continue
+ else:
+ if subcomponent.name() not in self.filter_name: continue
+ if self.match(subcomponent, access): break
+ else:
+ return not self.defined
+ return self.defined
+
+ def setInstances(self, instances):
+ """
+ Give the list of instances to each comp-filter element.
+ @param instances: the list of instances.
+ """
+ self.instances = instances
+ for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
+ compfilter.setInstances(instances)
+
+ def valid(self, level):
+ """
+ Indicate whether this filter element's structure is valid wrt iCalendar
+ data object model.
+
+ @param level: the nesting level of this filter element, 0 being the top comp-filter.
+ @return: True if valid, False otherwise
+ """
+
+ # Check for time-range
+ timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
+
+ if level == 0:
+ # Must have VCALENDAR at the top
+ if (self.filter_name != "VCALENDAR") or timerange:
+ log.msg("Top-level comp-filter must be VCALENDAR, instead: %s" % (self.filter_name,))
+ return False
+ elif level == 1:
+ # Disallow VCALENDAR, VALARM, STANDARD, DAYLIGHT, AVAILABLE at the top, everything else is OK
+ if self.filter_name in ("VCALENDAR", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE"):
+ log.msg("comp-filter wrong component type: %s" % (self.filter_name,))
+ return False
+
+ # time-range only on VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY
+ if timerange and self.filter_name not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY"):
+ log.msg("time-range cannot be used with component %s" % (self.filter_name,))
+ return False
+ elif level == 2:
+ # Disallow VCALENDAR, VTIMEZONE, VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY at the top, everything else is OK
+ if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY")):
+ log.msg("comp-filter wrong sub-component type: %s" % (self.filter_name,))
+ return False
+
+ # time-range only on VALARM, AVAILABLE
+ if timerange and self.filter_name not in ("VALARM", "AVAILABLE",):
+ log.msg("time-range cannot be used with sub-component %s" % (self.filter_name,))
+ return False
+ else:
+ # Disallow all standard iCal components anywhere else
+ if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE")) or timerange:
+ log.msg("comp-filter wrong standard component type: %s" % (self.filter_name,))
+ return False
+
+ # Test each property
+ for propfilter in [x for x in self.filters if isinstance(x, PropertyFilter)]:
+ if not propfilter.valid():
+ return False
+
+ # Test each component
+ for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
+ if not compfilter.valid(level + 1):
+ return False
+
+ # Test the time-range
+ if timerange:
+ if not self.qualifier.valid():
+ return False
+
+ return True
+
+ def settzinfo(self, tzinfo):
+ """
+ Set the default timezone to use with this query.
+ @param tzinfo: a L{datetime.tzinfo} to use.
+ """
+
+ # Give tzinfo to any TimeRange we have
+ if isinstance(self.qualifier, TimeRange):
+ self.qualifier.settzinfo(tzinfo)
+
+ # Pass down to sub components/properties
+ 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 (FilterChildBase):
+ """
+ Limits a search to specific properties.
+ """
+
+ def _match(self, component, access):
+ # When access restriction is in force, we need to only allow matches against the properties
+ # allowed by the access restriction level.
+ if access:
+ allowedProperties = Component.confidentialPropertiesMap.get(component.name(), None)
+ if allowedProperties and access == Component.ACCESS_RESTRICTED:
+ allowedProperties += Component.extraRestrictedProperties
+ else:
+ allowedProperties = None
+
+ # At least one property must match (or is-not-defined is set)
+ for property in component.properties():
+ # Apply access restrictions, if any.
+ if allowedProperties is not None and property.name() not in allowedProperties:
+ continue
+ if property.name() == self.filter_name and self.match(property, access): break
+ else:
+ return not self.defined
+ return self.defined
+
+ def valid(self):
+ """
+ Indicate whether this filter element's structure is valid wrt iCalendar
+ data object model.
+
+ @return: True if valid, False otherwise
+ """
+
+ # Check for time-range
+ timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
+
+ # time-range only on COMPLETED, CREATED, DTSTAMP, LAST-MODIFIED
+ if timerange and self.filter_name not in ("COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
+ log.msg("time-range cannot be used with property %s" % (self.filter_name,))
+ return False
+
+ # Test the time-range
+ if timerange:
+ if not self.qualifier.valid():
+ return False
+
+ # No other tests
+ return True
+
+ def settzinfo(self, tzinfo):
+ """
+ Set the default timezone to use with this query.
+ @param tzinfo: a L{datetime.tzinfo} to use.
+ """
+
+ # Give tzinfo to any TimeRange we have
+ 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 (FilterChildBase):
+ """
+ Limits a search to specific parameters.
+ """
+
+ def _match(self, property, access):
+
+ # We have to deal with the problem that the 'Native' form of a property
+ # will be missing the TZID parameter due to the conversion performed. Converting
+ # to non-native for the entire calendar object causes problems elsewhere, so its
+ # best to do it here for this one special case.
+ if self.filter_name == "TZID":
+ transformed = property.transformAllFromNative()
+ else:
+ transformed = False
+
+ # At least one property must match (or is-not-defined is set)
+ result = not self.defined
+ for parameterName in property.params().keys():
+ if parameterName == self.filter_name and self.match(property.params()[parameterName], access):
+ result = self.defined
+ break
+
+ if transformed:
+ property.transformAllToNative()
+ return result
+
+class IsNotDefined (FilterBase):
+ """
+ Specifies that the named iCalendar item does not exist.
+ """
+
+ def match(self, component, access=None):
+ # Oddly, this needs always to return True so that it appears there is
+ # a match - but we then "negate" the result if is-not-defined is set.
+ # Actually this method should never be called as we special case the
+ # is-not-defined option.
+ return True
+
+class TextMatch (FilterBase):
+ """
+ Specifies a substring match on a property or parameter value.
+ (CalDAV-access-09, section 9.6.4)
+ """
+ def __init__(self, xml_element):
+
+ super(TextMatch, self).__init__(xml_element)
+
+ self.text = str(xml_element)
+ if "caseless" in xml_element.attributes:
+ caseless = xml_element.attributes["caseless"]
+ if caseless == "yes":
+ self.caseless = True
+ elif caseless == "no":
+ self.caseless = False
+ else:
+ self.caseless = True
+
+ if "negate-condition" in xml_element.attributes:
+ negate = xml_element.attributes["negate-condition"]
+ if negate == "yes":
+ self.negate = True
+ elif caseless == "no":
+ self.negate = False
+ else:
+ self.negate = False
+
+ def match(self, item, access):
+ """
+ Match the text for the item.
+ If the item is a property, then match the property value,
+ otherwise it may be a list of parameter values - try to match anyone of those
+ """
+ if item is None: return False
+
+ if isinstance(item, Property):
+ values = [item.value()]
+ else:
+ values = item
+
+ test = unicode(self.text, "utf-8")
+ if self.caseless:
+ test = test.lower()
+
+ def _textCompare(s):
+ if self.caseless:
+ if s.lower().find(test) != -1:
+ return True, not self.negate
+ else:
+ if s.find(test) != -1:
+ return True, not self.negate
+ return False, False
+
+ for value in values:
+ # NB Its possible that we have a text list value which appears as a Python list,
+ # so we need to check for that an iterate over the list.
+ if isinstance(value, list):
+ for subvalue in value:
+ matched, result = _textCompare(subvalue)
+ if matched:
+ return result
+ else:
+ matched, result = _textCompare(value)
+ if matched:
+ return result
+
+ return self.negate
+
+class TimeRange (FilterBase):
+ """
+ Specifies a time for testing components against.
+ """
+
+ def __init__(self, xml_element):
+
+ super(TimeRange, self).__init__(xml_element)
+
+ # One of start or end must be present
+ if "start" not in xml_element.attributes and "end" not in xml_element.attributes:
+ raise ValueError("One of 'start' or 'end' must be present in CALDAV:time-range")
+
+ self.start = parse_date_or_datetime(xml_element.attributes["start"]) if "start" in xml_element.attributes else None
+ self.end = parse_date_or_datetime(xml_element.attributes["end"]) if "end" in xml_element.attributes else None
+ self.tzinfo = None
+
+ def settzinfo(self, tzinfo):
+ """
+ Set the default timezone to use with this query.
+ @param tzinfo: a L{datetime.tzinfo} to use.
+ """
+
+ # Give tzinfo to any TimeRange we have
+ self.tzinfo = tzinfo
+
+ def valid(self, level=0):
+ """
+ Indicate whether the time-range is valid (must be date-time in UTC).
+
+ @return: True if valid, False otherwise
+ """
+
+ 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 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 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 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
+ return True
+
+ def match(self, property, access=None):
+ """
+ NB This is only called when doing a time-range match on a property.
+ """
+ if property is None:
+ return False
+ else:
+ return property.containsTimeRange(self.start, self.end, self.tzinfo)
+
+ def matchinstance(self, component, instances):
+ """
+ Test whether this time-range element causes a match to the specified component
+ using the specified set of instances to determine the expanded time ranges.
+ @param component: the L{Component} to test.
+ @param instances: the list of expanded instances.
+ @return: True if the time-range query matches, False otherwise.
+ """
+ if component is None:
+ return False
+
+ 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")
+ if alarms:
+ testcomponent = component._parent
+ else:
+ testcomponent = component
+
+ for key in instances:
+ instance = instances[key]
+
+ # First make sure components match
+ if not testcomponent.same(instance.component):
+ continue
+
+ if alarms:
+ # Get all the alarm triggers for this instance and test each one
+ triggers = instance.getAlarmTriggers()
+ for trigger in triggers:
+ if timeRangesOverlap(trigger, None, self.start, self.end, self.tzinfo):
+ return True
+ else:
+ # Regular instance overlap test
+ if timeRangesOverlap(instance.start, instance.end, self.start, self.end, self.tzinfo):
+ return True
+
+ return False
Added: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/__init__.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Tests for the twistedcaldav.query module.
+"""
Added: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/test_calendarquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/test_calendarquery.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/test_calendarquery.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -0,0 +1,99 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav import caldavxml
+from twistedcaldav.query import queryfilter
+from twistedcaldav.query.calendarquery import sqlcalendarquery
+import datetime
+import twistedcaldav.test.util
+
+class Tests(twistedcaldav.test.util.TestCase):
+
+ def test_query(self):
+
+ filter = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ *[caldavxml.ComponentFilter(
+ *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ )],
+ **{"name":"VCALENDAR"}
+ )
+ )
+ filter = queryfilter.Filter(filter)
+
+ # A complete implementation of current DST rules for major US time zones.
+
+ def first_sunday_on_or_after(dt):
+ days_to_go = 6 - dt.weekday()
+ if days_to_go:
+ dt += datetime.timedelta(days_to_go)
+ return dt
+
+ # In the US, DST starts at 2am (standard time) on the first Sunday in April.
+ DSTSTART = datetime.datetime(1, 4, 1, 2)
+ # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
+ # which is the first Sunday on or after Oct 25.
+ DSTEND = datetime.datetime(1, 10, 25, 1)
+
+ ZERO = datetime.timedelta(0)
+ HOUR = datetime.timedelta(hours=1)
+
+ class USTimeZone(datetime.tzinfo):
+
+ def __init__(self, hours, reprname, stdname, dstname):
+ self.stdoffset = datetime.timedelta(hours=hours)
+ self.reprname = reprname
+ self.stdname = stdname
+ self.dstname = dstname
+
+ def __repr__(self):
+ return self.reprname
+
+ def tzname(self, dt):
+ if self.dst(dt):
+ return self.dstname
+ else:
+ return self.stdname
+
+ def utcoffset(self, dt):
+ return self.stdoffset + self.dst(dt)
+
+ def dst(self, dt):
+ if dt is None or dt.tzinfo is None:
+ # An exception may be sensible here, in one or both cases.
+ # It depends on how you want to treat them. The default
+ # fromutc() implementation (called by the default astimezone()
+ # implementation) passes a datetime with dt.tzinfo is self.
+ return ZERO
+ assert dt.tzinfo is self
+
+ # Find first Sunday in April & the last in October.
+ start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
+ end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
+
+ # Can't compare naive to aware objects, so strip the timezone from
+ # dt first.
+ if start <= dt.replace(tzinfo=None) < end:
+ return HOUR
+ else:
+ return ZERO
+
+ Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
+ filter.child.settzinfo(Eastern)
+
+ print sqlcalendarquery(filter)
+
\ No newline at end of file
Added: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/test_queryfilter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/test_queryfilter.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/query/test/test_queryfilter.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -0,0 +1,77 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav import caldavxml
+from twistedcaldav.query import queryfilter
+import twistedcaldav.test.util
+
+class Tests(twistedcaldav.test.util.TestCase):
+
+ def test_allQuery(self):
+
+ xml_element = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ **{"name":"VCALENDAR"}
+ )
+ )
+
+ queryfilter.Filter(xml_element)
+
+ def test_simpleSummaryRangeQuery(self):
+
+ xml_element = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ caldavxml.PropertyFilter(
+ caldavxml.TextMatch.fromString("test"),
+ **{"name":"SUMMARY",}
+ ),
+ **{"name":"VEVENT"}
+ ),
+ **{"name":"VCALENDAR"}
+ )
+ )
+
+ queryfilter.Filter(xml_element)
+
+ def test_simpleTimeRangeQuery(self):
+
+ xml_element = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"}),
+ **{"name":"VEVENT"}
+ ),
+ **{"name":"VCALENDAR"}
+ )
+ )
+
+ queryfilter.Filter(xml_element)
+
+ def test_multipleTimeRangeQuery(self):
+
+ xml_element = caldavxml.Filter(
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"}),
+ **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+ ),
+ **{"name":"VCALENDAR"}
+ )
+ )
+
+ queryfilter.Filter(xml_element)
+
\ No newline at end of file
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/scheduling/processing.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/scheduling/processing.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -29,7 +29,7 @@
from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
from twistedcaldav.scheduling.itip import iTipProcessing, iTIPRequestStatus
from twistedcaldav.scheduling.utils import getCalendarObjectForPrincipals
-from vobject.icalendar import utc
+from vobject.icalendar import dateTimeToString, utc
import datetime
import time
@@ -483,9 +483,10 @@
dt = dt.replace(tzinfo=tzinfo).astimezone(utc)
return dt
- tr = caldavxml.TimeRange(start="20000101", end="20000101")
- tr.start = makeTimedUTC(instance.start)
- tr.end = makeTimedUTC(instance.end)
+ tr = caldavxml.TimeRange(
+ start=dateTimeToString(makeTimedUTC(instance.start)),
+ end=dateTimeToString(makeTimedUTC(instance.end)),
+ )
yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid)
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_calendarquery.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_calendarquery.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -30,6 +30,7 @@
from twistedcaldav import caldavxml
from twistedcaldav import ical
from twistedcaldav.index import db_basename
+from twistedcaldav.query import queryfilter
class CalendarQuery (twistedcaldav.test.util.TestCase):
"""
@@ -116,7 +117,7 @@
cal = property.calendar()
instances = cal.expandTimeRanges(query_timerange.end)
vevents = [x for x in cal.subcomponents() if x.name() == "VEVENT"]
- if not query_timerange.matchinstance(vevents[0], instances):
+ if not queryfilter.TimeRange(query_timerange).matchinstance(vevents[0], instances):
self.fail("REPORT property %r returned calendar %s outside of request time range %r"
% (property, property.calendar, query_timerange))
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_index.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_index.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_index.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -16,22 +16,21 @@
from twisted.internet import reactor
from twisted.internet.task import deferLater
-
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import TimeRange
from twistedcaldav.ical import Component
from twistedcaldav.index import Index, default_future_expansion_duration,\
maximum_future_expansion_duration, IndexedSearchException,\
AbstractCalendarIndex
from twistedcaldav.index import ReservationError, MemcachedUIDReserver
from twistedcaldav.instance import InvalidOverriddenInstanceError
+from twistedcaldav.query import queryfilter
from twistedcaldav.test.util import InMemoryMemcacheProtocol
-import twistedcaldav.test.util
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import TimeRange
from vobject.icalendar import utc
-import sqlite3
-
import datetime
import os
+import sqlite3
+import twistedcaldav.test.util
class SQLIndexTests (twistedcaldav.test.util.TestCase):
"""
@@ -414,17 +413,18 @@
# Create fake filter element to match time-range
filter = caldavxml.Filter(
- caldavxml.ComponentFilter(
- caldavxml.ComponentFilter(
- TimeRange(
- start=trstart,
- end=trend,
- ),
- name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
- ),
- name="VCALENDAR",
- )
- )
+ caldavxml.ComponentFilter(
+ caldavxml.ComponentFilter(
+ TimeRange(
+ start=trstart,
+ end=trend,
+ ),
+ name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+ ),
+ name="VCALENDAR",
+ )
+ )
+ filter = queryfilter.Filter(filter)
resources = self.db.indexedSearch(filter, fbtype=True)
index_results = set()
Modified: CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_xml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_xml.py 2009-11-09 15:42:26 UTC (rev 4720)
+++ CalendarServer/branches/users/cdaboo/per-user-icalendar-4669/twistedcaldav/test/test_xml.py 2009-11-09 15:42:57 UTC (rev 4721)
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
+from twistedcaldav.query import queryfilter
import os
@@ -41,11 +42,13 @@
if has: no = "no "
else: no = ""
- if has != ComponentFilter(
+ if has != queryfilter.ComponentFilter(
ComponentFilter(
- name=component_name
- ),
- name="VCALENDAR"
+ ComponentFilter(
+ name=component_name
+ ),
+ name="VCALENDAR"
+ )
).match(self.calendar, None):
self.fail("Calendar has %s%s?" % (no, component_name))
@@ -60,14 +63,16 @@
if has: no = "no "
else: no = ""
- if has != ComponentFilter(
+ if has != queryfilter.ComponentFilter(
ComponentFilter(
- PropertyFilter(
- name=property_name
+ ComponentFilter(
+ PropertyFilter(
+ name=property_name
+ ),
+ name="VEVENT"
),
- name="VEVENT"
- ),
- name="VCALENDAR"
+ name="VCALENDAR"
+ )
).match(self.calendar, None):
self.fail("Calendar has %sVEVENT with %s?" % (no, property_name))
@@ -90,15 +95,17 @@
if has: no = "no "
else: no = ""
- if has != ComponentFilter(
+ if has != queryfilter.ComponentFilter(
ComponentFilter(
- PropertyFilter(
- TextMatch.fromString(uid, caseless=caseless),
- name="UID"
+ ComponentFilter(
+ PropertyFilter(
+ TextMatch.fromString(uid, caseless=caseless),
+ name="UID"
+ ),
+ name="VEVENT"
),
- name="VEVENT"
- ),
- name="VCALENDAR"
+ name="VCALENDAR"
+ )
).match(self.calendar, None):
self.fail("Calendar has %sVEVENT with UID %s? (caseless=%s)" % (no, uid, caseless))
@@ -127,13 +134,17 @@
if has: no = "no "
else: no = ""
- if has != Filter(ComponentFilter(
- ComponentFilter(
- TimeRange(start=start, end=end),
- name="VEVENT"
- ),
- name="VCALENDAR"
- )).match(self.calendar):
+ if has != queryfilter.Filter(
+ Filter(
+ ComponentFilter(
+ ComponentFilter(
+ TimeRange(start=start, end=end),
+ name="VEVENT"
+ ),
+ name="VCALENDAR"
+ )
+ )
+ ).match(self.calendar):
self.fail("Calendar has %sVEVENT with timerange %s?" % (no, (start, end)))
test_TimeRange.todo = "recurrence expansion"
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20091109/670eb3d5/attachment-0001.html>
More information about the calendarserver-changes
mailing list