1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-07-23 17:48:30 +00:00

(#1485) Fix processing all day expanded events

This commit is contained in:
Georgiy 2024-05-29 16:33:54 +03:00
parent 96b63ed65f
commit acf65e9d6a
3 changed files with 154 additions and 16 deletions

View file

@ -29,6 +29,7 @@ from typing import (Callable, Iterable, Iterator, List, Optional, Sequence,
from urllib.parse import unquote, urlparse from urllib.parse import unquote, urlparse
import vobject.base import vobject.base
from dateutil import rrule
from vobject.base import ContentLine from vobject.base import ContentLine
import radicale.item as radicale_item import radicale.item as radicale_item
@ -196,14 +197,10 @@ def _expand(
start: datetime.datetime, start: datetime.datetime,
end: datetime.datetime, end: datetime.datetime,
) -> ET.Element: ) -> ET.Element:
rruleset = None expanded_item, rruleset = _make_vobject_expanded_item(item)
if hasattr(item.vobject_item.vevent, 'rrule'):
rruleset = item.vobject_item.vevent.getrruleset()
expanded_item = _make_vobject_expanded_item(item)
if rruleset: if rruleset:
recurrences = rruleset.between(start, end) recurrences = rruleset.between(start, end, inc=True)
expanded: vobject.base.Component = copy.copy(expanded_item.vobject_item) expanded: vobject.base.Component = copy.copy(expanded_item.vobject_item)
is_expanded_filled: bool = False is_expanded_filled: bool = False
@ -232,7 +229,7 @@ def _expand(
def _make_vobject_expanded_item( def _make_vobject_expanded_item(
item: radicale_item.Item item: radicale_item.Item
) -> radicale_item.Item: ) -> Tuple[radicale_item.Item, Optional[rrule.rruleset]]:
# https://www.rfc-editor.org/rfc/rfc4791#section-9.6.5 # https://www.rfc-editor.org/rfc/rfc4791#section-9.6.5
# The returned calendar components MUST NOT use recurrence # The returned calendar components MUST NOT use recurrence
# properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT # properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT
@ -243,17 +240,33 @@ def _make_vobject_expanded_item(
item = copy.copy(item) item = copy.copy(item)
vevent = item.vobject_item.vevent vevent = item.vobject_item.vevent
start_utc = vevent.dtstart.value.astimezone(datetime.timezone.utc) if type(vevent.dtstart.value) is datetime.date:
vevent.dtstart = ContentLine( start_utc = datetime.datetime.fromordinal(
name='DTSTART', vevent.dtstart.value.toordinal()
value=start_utc.strftime('%Y%m%dT%H%M%SZ'), params={}) ).replace(tzinfo=datetime.timezone.utc)
else:
start_utc = vevent.dtstart.value.astimezone(datetime.timezone.utc)
vevent.dtstart = ContentLine(name='DTSTART', value=start_utc, params=[])
dt_end = getattr(vevent, 'dtend', None) dt_end = getattr(vevent, 'dtend', None)
if dt_end is not None: if dt_end is not None:
end_utc = dt_end.value.astimezone(datetime.timezone.utc) if type(vevent.dtend.value) is datetime.date:
vevent.dtend = ContentLine( end_utc = datetime.datetime.combine(
name='DTEND', dt_end.value, time=datetime.time(0, 0, 0)).replace(tzinfo=datetime.timezone.utc)
value=end_utc.strftime('%Y%m%dT%H%M%SZ'), params={}) else:
end_utc = dt_end.value.astimezone(datetime.timezone.utc)
vevent.dtend = ContentLine(name='DTEND', value=end_utc, params={})
rruleset = None
if hasattr(item.vobject_item.vevent, 'rrule'):
rruleset = vevent.getrruleset()
# There is something strage behavour during serialization native datetime, so converting manualy
vevent.dtstart.value = vevent.dtstart.value.strftime('%Y%m%dT%H%M%SZ')
if dt_end is not None:
vevent.dtend.value = vevent.dtend.value.strftime('%Y%m%dT%H%M%SZ')
timezones_to_remove = [] timezones_to_remove = []
for component in item.vobject_item.components(): for component in item.vobject_item.components():
@ -271,7 +284,7 @@ def _make_vobject_expanded_item(
except AttributeError: except AttributeError:
pass pass
return item return item, rruleset
def xml_item_response(base_prefix: str, href: str, def xml_item_response(base_prefix: str, href: str,

View file

@ -0,0 +1,31 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VTIMEZONE
LAST-MODIFIED:20040110T032845Z
TZID:US/Eastern
BEGIN:DAYLIGHT
DTSTART:20000404
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZNAME:EDT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20001026
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZNAME:EST
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060102
DTEND;TZID=US/Eastern:20060103
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Recurring event
UID:event_full_day_rrule
DTSTAMP:20060102T094829Z
END:VEVENT
END:VCALENDAR

View file

@ -1604,11 +1604,105 @@ permissions: RrWw""")
uids.append(line) uids.append(line)
if line.startswith("RECURRENCE-ID:"): if line.startswith("RECURRENCE-ID:"):
assert line in ["RECURRENCE-ID:20060103T170000Z", "RECURRENCE-ID:20060104T170000Z"]
recurrence_ids.append(line) recurrence_ids.append(line)
if line.startswith("DTSTART:"):
assert line == "DTSTART:20060102T170000Z"
assert len(uids) == 2 assert len(uids) == 2
assert len(set(recurrence_ids)) == 2 assert len(set(recurrence_ids)) == 2
def test_report_with_expand_property_all_day_event(self) -> None:
"""Test report with expand property"""
self.put("/calendar.ics/", get_file_content("event_full_day_rrule.ics"))
req_body_without_expand = \
"""<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20060103T000000Z" end="20060105T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"""
_, responses = self.report("/calendar.ics/", req_body_without_expand)
assert len(responses) == 1
response_without_expand = responses['/calendar.ics/event_full_day_rrule.ics']
assert not isinstance(response_without_expand, int)
status, element = response_without_expand["C:calendar-data"]
assert status == 200 and element.text
assert "RRULE" in element.text
assert "RECURRENCE-ID" not in element.text
uids: List[str] = []
for line in element.text.split("\n"):
if line.startswith("UID:"):
uid = line[len("UID:"):]
assert uid == "event_full_day_rrule"
uids.append(uid)
assert len(uids) == 1
req_body_with_expand = \
"""<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data>
<C:expand start="20060103T000000Z" end="20060105T000000Z"/>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20060103T000000Z" end="20060105T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"""
_, responses = self.report("/calendar.ics/", req_body_with_expand)
assert len(responses) == 1
response_with_expand = responses['/calendar.ics/event_full_day_rrule.ics']
assert not isinstance(response_with_expand, int)
status, element = response_with_expand["C:calendar-data"]
assert status == 200 and element.text
assert "RRULE" not in element.text
assert "BEGIN:VTIMEZONE" not in element.text
uids = []
recurrence_ids = []
for line in element.text.split("\n"):
if line.startswith("UID:"):
assert line == "UID:event_full_day_rrule"
uids.append(line)
if line.startswith("RECURRENCE-ID:"):
assert line in ["RECURRENCE-ID:20060103T000000Z", "RECURRENCE-ID:20060104T000000Z", "RECURRENCE-ID:20060105T000000Z"]
recurrence_ids.append(line)
if line.startswith("DTSTART:"):
assert line == "DTSTART:20060102T000000Z"
if line.startswith("DTEND:"):
assert line == "DTEND:20060103T000000Z"
assert len(uids) == 3
assert len(set(recurrence_ids)) == 3
def test_propfind_sync_token(self) -> None: def test_propfind_sync_token(self) -> None:
"""Retrieve the sync-token with a propfind request""" """Retrieve the sync-token with a propfind request"""
calendar_path = "/calendar.ics/" calendar_path = "/calendar.ics/"