diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py
index ebcd43b..daaf8d7 100644
--- a/radicale/xmlutils.py
+++ b/radicale/xmlutils.py
@@ -29,6 +29,7 @@ import posixpath
import re
import xml.etree.ElementTree as ET
from collections import OrderedDict
+from datetime import datetime, timedelta, timezone
from urllib.parse import unquote, urlparse
import vobject
@@ -146,9 +147,9 @@ def _comp_match(item, filter_, scope="collection"):
return filter_.get("name") != tag
if filter_[0].tag == _tag("C", "time-range"):
# Point #3 of rfc4791-9.7.1
- if not _time_range_match(item, filter_):
+ if not _time_range_match(item.item, filter_[0], tag):
return False
- filter_.remove(filter_[0])
+ filter_ = filter_[1:]
# Point #4 of rfc4791-9.7.1
return all(
_prop_match(item, child) if child.tag == _tag("C", "prop-filter")
@@ -180,26 +181,81 @@ def _prop_match(item, filter_):
return name not in vobject_item.contents
if filter_[0].tag == _tag("C", "time-range"):
# Point #3 of rfc4791-9.7.2
- if not _time_range_match(item, filter_[0]):
+ if not _time_range_match(vobject_item, filter_[0], name):
return False
- filter_.remove(filter_[0])
+ filter_ = filter_[1:]
elif filter_[0].tag == _tag("C", "text-match"):
# Point #4 of rfc4791-9.7.2
if not _text_match(vobject_item, filter_[0], name):
return False
- filter_.remove(filter_[0])
+ filter_ = filter_[1:]
return all(
_param_filter_match(vobject_item, param_filter, name)
for param_filter in filter_)
-def _time_range_match(item, filter_):
+def _time_range_match(vobject_item, filter_, child_name):
"""Check whether the ``item`` matches the time-range ``filter_``.
See rfc4791-9.9.
"""
- # TODO: implement this
+ start = filter_.get("start")
+ end = filter_.get("end")
+ if not start and not end:
+ return False
+ if start:
+ start = datetime.strptime(start, "%Y%m%dT%H%M%SZ")
+ else:
+ start = datetime.datetime.min
+ if end:
+ end = datetime.strptime(end, "%Y%m%dT%H%M%SZ")
+ else:
+ end = datetime.datetime.max
+ start = start.replace(tzinfo=timezone.utc)
+ end = end.replace(tzinfo=timezone.utc)
+ child = getattr(vobject_item, child_name.lower())
+
+ # Comments give the lines in the tables of the specification
+ if child_name == "VEVENT":
+ # TODO: check if there's a timezone
+ dtstart = child.dtstart.value
+ if not isinstance(dtstart, datetime):
+ dtstart_is_datetime = False
+ # TODO: changing dates to datetimes may be wrong because of tz
+ dtstart = datetime.combine(dtstart, datetime.min.time()).replace(
+ tzinfo=timezone.utc)
+ else:
+ dtstart_is_datetime = True
+ dtend = getattr(child, "dtend", None)
+ duration = getattr(child, "duration", None)
+ if dtend is not None:
+ # Line 1
+ dtend = dtend.value
+ if not isinstance(dtend, datetime):
+ dtend = datetime.combine(dtend, datetime.min.time()).replace(
+ tzinfo=timezone.utc)
+ return start < dtend and end > dtstart
+ elif duration is not None:
+ duration = duration.value
+ if duration.seconds > 0:
+ # Line 2
+ return start < dtstart + duration and end > dtstart
+ else:
+ # Line 3
+ return start <= dtstart and end > dtstart
+ elif dtstart_is_datetime:
+ # Line 4
+ return start <= dtstart and end > dtstart
+ else:
+ # Line 5
+ return start < dtstart + timedelta(days=1) and end > dtstart
+ elif child_name == "VTODO":
+ # TODO: implement this
+ pass
+ elif child_name == "VJOURNAL":
+ # TODO: implement this
+ pass
return True
@@ -491,11 +547,11 @@ def _propfind_response(path, item, props, user, write=False):
for href, _ in item.list():
event = item.get(href)
if "vtimezone" in event.contents:
- for timezone in event.vtimezone_list:
- timezones.add(timezone)
+ for timezone_ in event.vtimezone_list:
+ timezones.add(timezone_)
collection = vobject.iCalendar()
- for timezone in timezones:
- collection.add(timezone)
+ for timezone_ in timezones:
+ collection.add(timezone_)
element.text = collection.serialize()
elif tag == _tag("D", "displayname"):
element.text = item.get_meta("D:displayname") or item.path
diff --git a/tests/static/event.ics b/tests/static/event1.ics
similarity index 89%
rename from tests/static/event.ics
rename to tests/static/event1.ics
index 424d2c5..bc04d80 100644
--- a/tests/static/event.ics
+++ b/tests/static/event1.ics
@@ -23,12 +23,12 @@ BEGIN:VEVENT
CREATED:20130902T150157Z
LAST-MODIFIED:20130902T150158Z
DTSTAMP:20130902T150158Z
-UID:event
+UID:event1
SUMMARY:Event
ORGANIZER:mailto:unclesam@example.com
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Jane Doe:MAILTO:janedoe@example.com
ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="MAILTO:bob@host.com";PARTSTAT=ACCEPTED;CN=John Doe:MAILTO:johndoe@example.com
-DTSTART;TZID=Europe/Paris:20130902T180000
-DTEND;TZID=Europe/Paris:20130902T190000
+DTSTART;TZID=Europe/Paris:20130901T180000
+DTEND;TZID=Europe/Paris:20130901T190000
END:VEVENT
END:VCALENDAR
diff --git a/tests/static/event2.ics b/tests/static/event2.ics
new file mode 100644
index 0000000..6f934fe
--- /dev/null
+++ b/tests/static/event2.ics
@@ -0,0 +1,31 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Europe/Paris
+X-LIC-LOCATION:Europe/Paris
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20130902T150157Z
+LAST-MODIFIED:20130902T150158Z
+DTSTAMP:20130902T150158Z
+UID:event2
+SUMMARY:Event2
+DTSTART;TZID=Europe/Paris:20130902T180000
+DTEND;TZID=Europe/Paris:20130902T190000
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/static/event3.ics b/tests/static/event3.ics
new file mode 100644
index 0000000..18bbbe9
--- /dev/null
+++ b/tests/static/event3.ics
@@ -0,0 +1,31 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Europe/Paris
+X-LIC-LOCATION:Europe/Paris
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20130902T150157Z
+LAST-MODIFIED:20130902T150158Z
+DTSTAMP:20130902T150158Z
+UID:event3
+SUMMARY:Event3
+DTSTART;TZID=Europe/Paris:20130903
+DURATION:PT1H
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/static/event4.ics b/tests/static/event4.ics
new file mode 100644
index 0000000..b4f3f82
--- /dev/null
+++ b/tests/static/event4.ics
@@ -0,0 +1,30 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Europe/Paris
+X-LIC-LOCATION:Europe/Paris
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20130902T150157Z
+LAST-MODIFIED:20130902T150158Z
+DTSTAMP:20130902T150158Z
+UID:event4
+SUMMARY:Event4
+DTSTART;TZID=Europe/Paris:20130904T180000
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/static/event5.ics b/tests/static/event5.ics
new file mode 100644
index 0000000..e87af37
--- /dev/null
+++ b/tests/static/event5.ics
@@ -0,0 +1,30 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Europe/Paris
+X-LIC-LOCATION:Europe/Paris
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20130902T150157Z
+LAST-MODIFIED:20130902T150158Z
+DTSTAMP:20130902T150158Z
+UID:event5
+SUMMARY:Event5
+DTSTART;TZID=Europe/Paris:20130905
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/test_base.py b/tests/test_base.py
index 7609083..ef4745d 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -54,8 +54,8 @@ class BaseRequests:
"""Add an event."""
self.request(
"PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
- event = get_file_content("event.ics")
- path = "/calendar.ics/event.ics"
+ event = get_file_content("event1.ics")
+ path = "/calendar.ics/event1.ics"
status, headers, answer = self.request("PUT", path, event)
assert status == 201
status, headers, answer = self.request("GET", path)
@@ -83,8 +83,8 @@ class BaseRequests:
"""Delete an event."""
self.request(
"PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
- event = get_file_content("event.ics")
- path = "/calendar.ics/event.ics"
+ event = get_file_content("event1.ics")
+ path = "/calendar.ics/event1.ics"
status, headers, answer = self.request("PUT", path, event)
# Then we send a DELETE request
status, headers, answer = self.request("DELETE", path)
@@ -93,13 +93,14 @@ class BaseRequests:
status, headers, answer = self.request("GET", "/calendar.ics/")
assert "VEVENT" not in answer
- def _test_filter(self, filters):
+ def _test_filter(self, filters, events=1):
filters_text = "".join(
"%s" % filter_ for filter_ in filters)
self.request(
"PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
- event = get_file_content("event.ics")
- self.request("PUT", "/calendar.ics/event.ics", event)
+ for i in range(events):
+ event = get_file_content("event%i.ics" % (i + 1))
+ self.request("PUT", "/calendar.ics/event%i.ics" % (i + 1), event)
status, headers, answer = self.request(
"REPORT", "/calendar.ics",
"""
@@ -113,29 +114,29 @@ class BaseRequests:
def test_calendar_tag_filter(self):
"""Report request with tag-based filter on calendar."""
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
"""])
def test_item_tag_filter(self):
"""Report request with tag-based filter on an item."""
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
"""])
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
"""])
def test_item_not_tag_filter(self):
"""Report request with tag-based is-not filter on an item."""
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
"""])
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
@@ -144,13 +145,13 @@ class BaseRequests:
def test_item_prop_filter(self):
"""Report request with prop-based filter on an item."""
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
"""])
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
@@ -159,7 +160,7 @@ class BaseRequests:
def test_item_not_prop_filter(self):
"""Report request with prop-based is-not filter on an item."""
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
@@ -167,7 +168,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
@@ -178,7 +179,7 @@ class BaseRequests:
def test_mutiple_filters(self):
"""Report request with multiple filters on an item."""
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
@@ -193,7 +194,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
@@ -206,7 +207,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
@@ -218,7 +219,7 @@ class BaseRequests:
def test_text_match_filter(self):
"""Report request with text-match filter on calendar."""
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
@@ -226,7 +227,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
@@ -234,7 +235,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
@@ -242,7 +243,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
@@ -253,7 +254,7 @@ class BaseRequests:
def test_param_filter(self):
"""Report request with param-filter on calendar."""
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
@@ -264,7 +265,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
@@ -275,7 +276,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" not in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" not in self._test_filter(["""
@@ -285,7 +286,7 @@ class BaseRequests:
"""])
- assert "href>/calendar.ics/event.ics" in self._test_filter(["""
+ assert "href>/calendar.ics/event1.ics" in self._test_filter(["""
@@ -296,6 +297,80 @@ class BaseRequests:
"""])
+ def test_time_range_filter(self):
+ """Report request with time-range filter on calendar."""
+ answer = self._test_filter(["""
+
+
+
+
+ """], events=5)
+ assert "href>/calendar.ics/event1.ics" in answer
+ assert "href>/calendar.ics/event2.ics" in answer
+ assert "href>/calendar.ics/event3.ics" in answer
+ assert "href>/calendar.ics/event4.ics" in answer
+ assert "href>/calendar.ics/event5.ics" in answer
+ answer = self._test_filter(["""
+
+
+
+
+
+
+
+
+
+ """], events=5)
+ assert "href>/calendar.ics/event1.ics" not in answer
+ assert "href>/calendar.ics/event2.ics" not in answer
+ assert "href>/calendar.ics/event3.ics" not in answer
+ assert "href>/calendar.ics/event4.ics" not in answer
+ assert "href>/calendar.ics/event5.ics" not in answer
+ answer = self._test_filter(["""
+
+
+
+
+ """], events=5)
+ assert "href>/calendar.ics/event1.ics" not in answer
+ assert "href>/calendar.ics/event2.ics" in answer
+ assert "href>/calendar.ics/event3.ics" in answer
+ assert "href>/calendar.ics/event4.ics" in answer
+ assert "href>/calendar.ics/event5.ics" in answer
+ answer = self._test_filter(["""
+
+
+
+
+ """], events=5)
+ assert "href>/calendar.ics/event1.ics" not in answer
+ assert "href>/calendar.ics/event2.ics" not in answer
+ assert "href>/calendar.ics/event3.ics" in answer
+ assert "href>/calendar.ics/event4.ics" in answer
+ assert "href>/calendar.ics/event5.ics" in answer
+ answer = self._test_filter(["""
+
+
+
+
+ """], events=5)
+ assert "href>/calendar.ics/event1.ics" not in answer
+ assert "href>/calendar.ics/event2.ics" not in answer
+ assert "href>/calendar.ics/event3.ics" in answer
+ assert "href>/calendar.ics/event4.ics" not in answer
+ assert "href>/calendar.ics/event5.ics" not in answer
+ answer = self._test_filter(["""
+
+
+
+
+ """], events=5)
+ assert "href>/calendar.ics/event1.ics" not in answer
+ assert "href>/calendar.ics/event2.ics" not in answer
+ assert "href>/calendar.ics/event3.ics" not in answer
+ assert "href>/calendar.ics/event4.ics" not in answer
+ assert "href>/calendar.ics/event5.ics" not in answer
+
class TestMultiFileSystem(BaseRequests, BaseTest):
"""Base class for filesystem tests."""