Support recurring items in time filters

Fix #33.
This commit is contained in:
Guillaume Ayoub 2016-07-01 16:40:43 +02:00
parent a14cc326fc
commit c294477aee
7 changed files with 246 additions and 111 deletions

View File

@ -118,6 +118,20 @@ def _href(collection, href):
href.lstrip("/")) href.lstrip("/"))
def _date_to_datetime(date_):
"""Transform a date to a UTC datetime.
If date_ is a datetime without timezone, return as UTC datetime. If date_
is already a datetime with timezone, return as is.
"""
if not isinstance(date_, datetime):
date_ = datetime.combine(date_, datetime.min.time())
if not date_.tzinfo:
date_ = date_.replace(tzinfo=timezone.utc)
return date_
def _comp_match(item, filter_, scope="collection"): def _comp_match(item, filter_, scope="collection"):
"""Check whether the ``item`` matches the comp ``filter_``. """Check whether the ``item`` matches the comp ``filter_``.
@ -220,126 +234,156 @@ def _time_range_match(vobject_item, filter_, child_name):
if child_name == "VEVENT": if child_name == "VEVENT":
# TODO: check if there's a timezone # TODO: check if there's a timezone
dtstart = child.dtstart.value dtstart = child.dtstart.value
if isinstance(dtstart, datetime) and not dtstart.tzinfo:
dtstart = dtstart.replace(tzinfo=timezone.utc)
if not isinstance(dtstart, datetime): if child.rruleset:
dtstart_is_datetime = False dtstarts = child.getrruleset(addRDate=True)
# TODO: changing dates to datetimes may be wrong because of tz
dtstart = datetime.combine(dtstart, datetime.min.time()).replace(
tzinfo=timezone.utc)
else: else:
dtstart_is_datetime = True dtstarts = (dtstart,)
dtend = getattr(child, "dtend", None) dtend = getattr(child, "dtend", None)
duration = getattr(child, "duration", None)
if dtend is not None: if dtend is not None:
# Line 1
dtend = dtend.value dtend = dtend.value
if not isinstance(dtend, datetime): original_duration = (dtend - dtstart).total_seconds()
dtend = datetime.combine(dtend, datetime.min.time()).replace( dtend = _date_to_datetime(dtend)
tzinfo=timezone.utc)
if isinstance(dtend, datetime) and not dtend.tzinfo: duration = getattr(child, "duration", None)
dtend = dtend.replace(tzinfo=timezone.utc) if duration is not None:
original_duration = duration = duration.value
for dtstart in dtstarts:
dtstart_is_datetime = isinstance(dtstart, datetime)
dtstart = _date_to_datetime(dtstart)
if dtstart > end:
break
if dtend is not None:
# Line 1
dtend = dtstart + timedelta(seconds=original_duration)
if start < dtend and end > dtstart:
return True
elif duration is not None:
if original_duration is None:
original_duration = duration.seconds
if duration.seconds > 0:
# Line 2
if start < dtstart + duration and end > dtstart:
return True
elif start <= dtstart and end > dtstart:
# Line 3
return True
elif dtstart_is_datetime:
# Line 4
if start <= dtstart and end > dtstart:
return True
elif start < dtstart + timedelta(days=1) and end > dtstart:
# Line 5
return True
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": elif child_name == "VTODO":
# TODO: implement this
dtstart = getattr(child, "dtstart", None) dtstart = getattr(child, "dtstart", None)
duration = getattr(child, "duration", None) duration = getattr(child, "duration", None)
due = getattr(child, "due", None) due = getattr(child, "due", None)
completed = getattr(child, "completed", None) completed = getattr(child, "completed", None)
created = getattr(child, "created", None) created = getattr(child, "created", None)
if dtstart is not None and duration is not None: if dtstart is not None:
# Line 1 dtstart = _date_to_datetime(dtstart.value)
dtstart = dtstart.value if duration is not None:
if not isinstance(dtstart, datetime):
dtstart = (datetime.combine(dtstart, datetime.min.time())
.replace(tzinfo=timezone.utc))
duration = duration.value duration = duration.value
return (start <= dtstart + duration and if due is not None:
(end > dtstart or end >= dtstart + duration)) due = _date_to_datetime(due.value)
elif dtstart is not None and due is not None: if dtstart is not None:
# Line 2 original_duration = (due - dtstart).total_seconds()
dtstart = dtstart.value if completed is not None:
if not isinstance(dtstart, datetime): completed = _date_to_datetime(completed.value)
dtstart = (datetime.combine(dtstart, datetime.min.time()) if created is not None:
.replace(tzinfo=timezone.utc)) created = _date_to_datetime(created.value)
due = due.value original_duration = (completed - created).total_seconds()
if not isinstance(due, datetime):
due = datetime.combine(due, datetime.min.time()).replace(
tzinfo=timezone.utc)
return ((start < due or start <= dtstart) and
(end > dtstart or end >= due))
elif dtstart is not None:
# Line 3
dtstart = dtstart.value
if not isinstance(dtstart, datetime):
dtstart = (datetime.combine(dtstart, datetime.min.time())
.replace(tzinfo=timezone.utc))
return start <= dtstart and end > dtstart
elif due is not None:
# Line 4
due = due.value
if not isinstance(due, datetime):
due = datetime.combine(due, datetime.min.time()).replace(
tzinfo=timezone.utc)
return start < due and end >= due
elif completed is not None and created is not None:
# Line 5
completed = completed.value
created = created.value
return ((start <= created or start <= completed) and
(end >= created or end >= completed))
elif completed is not None:
# Line 6
completed = completed.value
return start <= completed and end >= completed
elif created is not None: elif created is not None:
# Line 7 created = _date_to_datetime(created.value)
created = created.value
return end > created if child.rruleset:
reference_dates = child.getrruleset(addRDate=True)
else: else:
return True if dtstart is not None:
reference_dates = (dtstart,)
elif due is not None:
reference_dates = (due,)
elif completed is not None:
reference_dates = (completed,)
elif created is not None:
reference_dates = (created,)
else:
# Line 8
return True
for reference_date in reference_dates:
reference_date = _date_to_datetime(reference_date)
if reference_date > end:
break
if dtstart is not None and duration is not None:
# Line 1
if start <= reference_date + duration and (
end > reference_date or
end >= reference_date + duration):
return True
elif dtstart is not None and due is not None:
# Line 2
due = reference_date + timedelta(seconds=original_duration)
if (start < due or start <= reference_date) and (
end > reference_date or end >= due):
return True
elif dtstart is not None:
if start <= reference_date and end > reference_date:
return True
elif due is not None:
# Line 4
if start < reference_date and end >= reference_date:
return True
elif completed is not None and created is not None:
# Line 5
completed = reference_date + timedelta(
seconds=original_duration)
if (start <= reference_date or start <= completed) and (
end >= reference_date or end >= completed):
return True
elif completed is not None:
# Line 6
if start <= reference_date and end >= reference_date:
return True
elif created is not None:
# Line 7
if end > reference_date:
return True
elif child_name == "VJOURNAL": elif child_name == "VJOURNAL":
dtstart = getattr(child, "dtstart", None) dtstart = getattr(child, "dtstart", None)
if dtstart is not None: if dtstart is not None:
dtstart = dtstart.value dtstart = dtstart.value
if not isinstance(dtstart, datetime): if child.rruleset:
dtstart_is_datetime = False dtstarts = child.getrruleset(addRDate=True)
# TODO: changing dates to datetimes may be wrong because of tz
dtstart = (datetime.combine(dtstart, datetime.min.time())
.replace(tzinfo=timezone.utc))
else: else:
dtstart_is_datetime = True dtstarts = (dtstart,)
if dtstart_is_datetime: for dtstart in dtstarts:
# Line 1 dtstart_is_datetime = isinstance(dtstart, datetime)
return start <= dtstart and end > dtstart dtstart = _date_to_datetime(dtstart)
else:
# Line 2
return start < dtstart + timedelta(days=1) and end > dtstart
else:
# Line 3
return False
return True if dtstart > end:
break
if dtstart_is_datetime:
# Line 1
if start <= dtstart and end > dtstart:
return True
elif start < dtstart + timedelta(days=1) and end > dtstart:
# Line 2
return True
return False
def _text_match(vobject_item, filter_, child_name, attrib_name=None): def _text_match(vobject_item, filter_, child_name, attrib_name=None):

View File

@ -27,5 +27,6 @@ UID:event2
SUMMARY:Event2 SUMMARY:Event2
DTSTART;TZID=Europe/Paris:20130902T180000 DTSTART;TZID=Europe/Paris:20130902T180000
DTEND;TZID=Europe/Paris:20130902T190000 DTEND;TZID=Europe/Paris:20130902T190000
RRULE:FREQ=WEEKLY
END:VEVENT END:VEVENT
END:VCALENDAR END:VCALENDAR

View File

@ -25,7 +25,8 @@ UID:journal2
DTSTAMP:19950817T000000 DTSTAMP:19950817T000000
DTSTART;TZID=Europe/Paris:20000101T100000 DTSTART;TZID=Europe/Paris:20000101T100000
SUMMARY:happy new year SUMMARY:happy new year
DESCRIPTION: Happy new year 2001 ! DESCRIPTION: Happy new year !
RRULE:FREQ=YEARLY
END:VJOURNAL END:VJOURNAL
END:VCALENDAR END:VCALENDAR

View File

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTODO
CREATED:20130903T091105Z
LAST-MODIFIED:20130903T091108Z
DTSTAMP:20130903T091108Z
UID:todo
SUMMARY:Todo
END:VTODO
END:VCALENDAR

View File

@ -22,5 +22,7 @@ END:VTIMEZONE
BEGIN:VTODO BEGIN:VTODO
DTSTART;TZID=Europe/Paris:20130901T220000 DTSTART;TZID=Europe/Paris:20130901T220000
DURATION:PT1H DURATION:PT1H
SUMMARY:Todo
UID:todo
END:VTODO END:VTODO
END:VCALENDAR END:VCALENDAR

View File

@ -22,5 +22,6 @@ END:VTIMEZONE
BEGIN:VTODO BEGIN:VTODO
DTSTART;TZID=Europe/Paris:20130901T180000 DTSTART;TZID=Europe/Paris:20130901T180000
DUE;TZID=Europe/Paris:20130903T180000 DUE;TZID=Europe/Paris:20130903T180000
RRULE:FREQ=MONTHLY
END:VTODO END:VTODO
END:VCALENDAR END:VCALENDAR

View File

@ -69,8 +69,8 @@ class BaseRequests:
"""Add a todo.""" """Add a todo."""
self.request( self.request(
"PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR") "PUT", "/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR")
todo = get_file_content("todo.ics") todo = get_file_content("todo1.ics")
path = "/calendar.ics/todo.ics" path = "/calendar.ics/todo1.ics"
status, headers, answer = self.request("PUT", path, todo) status, headers, answer = self.request("PUT", path, todo)
assert status == 201 assert status == 201
status, headers, answer = self.request("GET", path) status, headers, answer = self.request("GET", path)
@ -341,7 +341,7 @@ class BaseRequests:
answer = self._test_filter([""" answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR"> <C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT"> <C:comp-filter name="VEVENT">
<C:time-range start="20130903T000000Z" end="20131001T000000Z"/> <C:time-range start="20130903T000000Z" end="20130908T000000Z"/>
</C:comp-filter> </C:comp-filter>
</C:comp-filter>"""], items=5) </C:comp-filter>"""], items=5)
assert "href>/calendar.ics/event1.ics</" not in answer assert "href>/calendar.ics/event1.ics</" not in answer
@ -372,6 +372,41 @@ class BaseRequests:
assert "href>/calendar.ics/event4.ics</" not in answer assert "href>/calendar.ics/event4.ics</" not in answer
assert "href>/calendar.ics/event5.ics</" not in answer assert "href>/calendar.ics/event5.ics</" not in answer
def test_time_range_filter_events_rrule(self):
"""Report request with time-range filter on events with rrules."""
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=2)
assert "href>/calendar.ics/event1.ics</" in answer
assert "href>/calendar.ics/event2.ics</" in answer
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=2)
assert "href>/calendar.ics/event1.ics</" not in answer
assert "href>/calendar.ics/event2.ics</" in answer
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20120801T000000Z" end="20121001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=2)
assert "href>/calendar.ics/event1.ics</" not in answer
assert "href>/calendar.ics/event2.ics</" not in answer
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20130903T000000Z" end="20130907T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "event", items=2)
assert "href>/calendar.ics/event1.ics</" not in answer
assert "href>/calendar.ics/event2.ics</" not in answer
def test_time_range_filter_todos(self): def test_time_range_filter_todos(self):
"""Report request with time-range filter on todos.""" """Report request with time-range filter on todos."""
answer = self._test_filter([""" answer = self._test_filter(["""
@ -431,6 +466,41 @@ class BaseRequests:
</C:comp-filter>"""], "todo", items=8) </C:comp-filter>"""], "todo", items=8)
assert "href>/calendar.ics/todo7.ics</" in answer assert "href>/calendar.ics/todo7.ics</" in answer
def test_time_range_filter_todos_rrule(self):
"""Report request with time-range filter on todos with rrules."""
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20130801T000000Z" end="20131001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=2)
assert "href>/calendar.ics/todo1.ics</" in answer
assert "href>/calendar.ics/todo2.ics</" in answer
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20140801T000000Z" end="20141001T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=2)
assert "href>/calendar.ics/todo1.ics</" not in answer
assert "href>/calendar.ics/todo2.ics</" in answer
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20140902T000000Z" end="20140903T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=2)
assert "href>/calendar.ics/todo1.ics</" not in answer
assert "href>/calendar.ics/todo2.ics</" in answer
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:time-range start="20140904T000000Z" end="20140914T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "todo", items=2)
assert "href>/calendar.ics/todo1.ics</" not in answer
assert "href>/calendar.ics/todo2.ics</" not in answer
def test_time_range_filter_journals(self): def test_time_range_filter_journals(self):
"""Report request with time-range filter on journals.""" """Report request with time-range filter on journals."""
answer = self._test_filter([""" answer = self._test_filter(["""
@ -467,7 +537,7 @@ class BaseRequests:
</C:comp-filter> </C:comp-filter>
</C:comp-filter>"""], "journal", items=3) </C:comp-filter>"""], "journal", items=3)
assert "href>/calendar.ics/journal1.ics</" not in answer assert "href>/calendar.ics/journal1.ics</" not in answer
assert "href>/calendar.ics/journal2.ics</" not in answer assert "href>/calendar.ics/journal2.ics</" in answer
assert "href>/calendar.ics/journal3.ics</" not in answer assert "href>/calendar.ics/journal3.ics</" not in answer
answer = self._test_filter([""" answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR"> <C:comp-filter name="VCALENDAR">
@ -479,6 +549,33 @@ class BaseRequests:
assert "href>/calendar.ics/journal2.ics</" in answer assert "href>/calendar.ics/journal2.ics</" in answer
assert "href>/calendar.ics/journal3.ics</" in answer assert "href>/calendar.ics/journal3.ics</" in answer
def test_time_range_filter_journals_rrule(self):
"""Report request with time-range filter on journals with rrules."""
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="19991229T000000Z" end="20000202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=2)
assert "href>/calendar.ics/journal1.ics</" not in answer
assert "href>/calendar.ics/journal2.ics</" in answer
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="20051229T000000Z" end="20060202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=2)
assert "href>/calendar.ics/journal1.ics</" not in answer
assert "href>/calendar.ics/journal2.ics</" in answer
answer = self._test_filter(["""
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VJOURNAL">
<C:time-range start="20060102T000000Z" end="20060202T000000Z"/>
</C:comp-filter>
</C:comp-filter>"""], "journal", items=2)
assert "href>/calendar.ics/journal1.ics</" not in answer
assert "href>/calendar.ics/journal2.ics</" not in answer
class TestMultiFileSystem(BaseRequests, BaseTest): class TestMultiFileSystem(BaseRequests, BaseTest):
"""Base class for filesystem tests.""" """Base class for filesystem tests."""