Fix: time-range filter matches VEVENT with whole-day DURATION
A calendar-query REPORT with a time-range filter failed to return a VEVENT that has a whole-day DURATION (e.g. DURATION:P1D or P2D) whenever the queried range fell inside the event but after DTSTART. The VEVENT time-range logic in radicale/item/filter.py gated the "non-zero duration" branch (rfc4791-9.9 line 2) on timedelta.seconds instead of timedelta.total_seconds(). For a duration that is an exact multiple of 24h, timedelta.seconds is 0 (the days component holds the value), so the event was treated as zero-length (line 3) and only matched a one-second window at its start. An identical event expressed with DTEND matched correctly, confirming the defect is isolated to the DURATION path. Use total_seconds() so multi-day durations are handled correctly. Adds a regression test (event11, DURATION:P2D) covering both an inside-range match and an outside-range non-match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 3.7.7.dev
|
||||||
|
* Fix: time-range filter treated a VEVENT with a whole-day DURATION (e.g. P1D, P2D) as zero-length (timedelta.seconds instead of total_seconds), so such events were missing from calendar-query REPORT results
|
||||||
|
|
||||||
## 3.7.6
|
## 3.7.6
|
||||||
* Extension: item verification on commandline
|
* Extension: item verification on commandline
|
||||||
* Improvement: catch lack of support of PERIOD in vobject <= 0.9.9
|
* Improvement: catch lack of support of PERIOD in vobject <= 0.9.9
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "Radicale"
|
|||||||
# When the version is updated, a new section in the CHANGELOG.md file must be
|
# When the version is updated, a new section in the CHANGELOG.md file must be
|
||||||
# added too.
|
# added too.
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
version = "3.7.6"
|
version = "3.7.7.dev"
|
||||||
authors = [{name = "Guillaume Ayoub", email = "guillaume.ayoub@kozea.fr"}, {name = "Unrud", email = "unrud@outlook.com"}, {name = "Peter Bieringer", email = "pb@bieringer.de"}]
|
authors = [{name = "Guillaume Ayoub", email = "guillaume.ayoub@kozea.fr"}, {name = "Unrud", email = "unrud@outlook.com"}, {name = "Peter Bieringer", email = "pb@bieringer.de"}]
|
||||||
license = {text = "GNU GPL v3"}
|
license = {text = "GNU GPL v3"}
|
||||||
description = "CalDAV and CardDAV Server"
|
description = "CalDAV and CardDAV Server"
|
||||||
|
|||||||
@@ -402,8 +402,8 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
|
|||||||
return
|
return
|
||||||
elif duration is not None:
|
elif duration is not None:
|
||||||
if original_duration is None:
|
if original_duration is None:
|
||||||
original_duration = duration.seconds
|
original_duration = duration.total_seconds()
|
||||||
if duration.seconds > 0:
|
if duration.total_seconds() > 0:
|
||||||
# Line 2
|
# Line 2
|
||||||
if range_fn(dtstart, dtstart + duration,
|
if range_fn(dtstart, dtstart + duration,
|
||||||
is_recurrence):
|
is_recurrence):
|
||||||
|
|||||||
11
radicale/tests/static/event11.ics
Normal file
11
radicale/tests/static/event11.ics
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Radicale//NONSGML Radicale Server//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTAMP:20130901T000000Z
|
||||||
|
UID:event11
|
||||||
|
SUMMARY:event11
|
||||||
|
DTSTART:20130901T000000Z
|
||||||
|
DURATION:P2D
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
@@ -1789,6 +1789,31 @@ permissions: RrWw""")
|
|||||||
</C:comp-filter>"""], items=(9,))
|
</C:comp-filter>"""], items=(9,))
|
||||||
assert "/calendar.ics/event9.ics" not in answer
|
assert "/calendar.ics/event9.ics" not in answer
|
||||||
|
|
||||||
|
def test_time_range_filter_events_whole_day_duration(self) -> None:
|
||||||
|
"""Report time-range filter on an event with a whole-day DURATION.
|
||||||
|
|
||||||
|
event11 starts 2013-09-01T00:00:00Z and lasts DURATION:P2D, i.e. it is
|
||||||
|
ongoing until 2013-09-03T00:00:00Z. A time-range that falls inside the
|
||||||
|
span (but after DTSTART) must match it, exactly as it would for an
|
||||||
|
equivalent event expressed with DTEND.
|
||||||
|
"""
|
||||||
|
# Time-range fully inside the 2-day event, after DTSTART.
|
||||||
|
answer = self._test_filter(["""\
|
||||||
|
<C:comp-filter name="VCALENDAR">
|
||||||
|
<C:comp-filter name="VEVENT">
|
||||||
|
<C:time-range start="20130902T000000Z" end="20130902T120000Z"/>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:comp-filter>"""], items=(11,))
|
||||||
|
assert "/calendar.ics/event11.ics" in answer
|
||||||
|
# Time-range fully after the event must not match.
|
||||||
|
answer = self._test_filter(["""\
|
||||||
|
<C:comp-filter name="VCALENDAR">
|
||||||
|
<C:comp-filter name="VEVENT">
|
||||||
|
<C:time-range start="20130904T000000Z" end="20130905T000000Z"/>
|
||||||
|
</C:comp-filter>
|
||||||
|
</C:comp-filter>"""], items=(11,))
|
||||||
|
assert "/calendar.ics/event11.ics" not in answer
|
||||||
|
|
||||||
def test_time_range_filter_without_comp_filter(self) -> None:
|
def test_time_range_filter_without_comp_filter(self) -> None:
|
||||||
"""Report request with time-range filter without comp-filter on events."""
|
"""Report request with time-range filter without comp-filter on events."""
|
||||||
answer = self._test_filter(["""\
|
answer = self._test_filter(["""\
|
||||||
|
|||||||
Reference in New Issue
Block a user