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:
TowyTowy
2026-07-11 15:58:39 +02:00
parent 21ad7466c5
commit e557c0cd5e
5 changed files with 42 additions and 3 deletions

View 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

View File

@@ -1789,6 +1789,31 @@ permissions: RrWw""")
</C:comp-filter>"""], items=(9,))
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:
"""Report request with time-range filter without comp-filter on events."""
answer = self._test_filter(["""\