diff --git a/caldav/objects.py b/caldav/objects.py index a08a808..9e14022 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -2027,7 +2027,8 @@ def expand_rrule(self, start: datetime, end: datetime) -> None: calendar = self.icalendar_instance calendar.subcomponents = [] for occurrence in recurrings: - occurrence.add("RECURRENCE-ID", occurrence.get("DTSTART")) + if "RECURRENCE-ID" not in occurrence: + occurrence.add("RECURRENCE-ID", occurrence.get("DTSTART")) calendar.add_component(occurrence) # add other components (except for the VEVENT itself and VTIMEZONE which is not allowed on occurrence events) for component in stripped_event.icalendar_instance.subcomponents: diff --git a/tests/test_caldav.py b/tests/test_caldav.py index c6dfb21..b0efa1b 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -21,6 +21,7 @@ from datetime import timedelta from datetime import timezone +import icalendar import pytest import requests import vobject @@ -174,6 +175,35 @@ END:VEVENT END:VCALENDAR""" +# example created by editing a specific occurrence of a recurrent event via Thunderbird +evr2 = """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +BEGIN:VEVENT +UID:c26921f4-0653-11ef-b756-58ce2a14e2e5 +DTSTART;VALUE=DATE:20240411 +DTEND;VALUE=DATE:20240412 +DTSTAMP:20240429T181103Z +LAST-MODIFIED:20240429T181103Z +RRULE:FREQ=WEEKLY;INTERVAL=2 +SEQUENCE:1 +SUMMARY:Test +X-MOZ-GENERATION:1 +END:VEVENT +BEGIN:VEVENT +UID:c26921f4-0653-11ef-b756-58ce2a14e2e5 +RECURRENCE-ID;VALUE=DATE:20240425 +DTSTART;VALUE=DATE:20240425 +DTEND;VALUE=DATE:20240426 +CREATED:20240429T181031Z +DTSTAMP:20240429T181103Z +LAST-MODIFIED:20240429T181103Z +SEQUENCE:1 +SUMMARY:Test (edited) +X-MOZ-GENERATION:1 +END:VEVENT +END:VCALENDAR""" + # example from http://www.rfc-editor.org/rfc/rfc5545.txt todo = """BEGIN:VCALENDAR VERSION:2.0 @@ -2505,7 +2535,7 @@ def testRecurringDateSearch(self): self.skip_on_compatibility_flag("no_search") c = self._fixCalendar() - # evr is a yearly event starting at 1997-02-11 + # evr is a yearly event starting at 1997-11-02 e = c.save_event(evr) ## Without "expand", we should still find it when searching over 2008 ... @@ -2583,6 +2613,30 @@ def testRecurringDateSearch(self): assert len(r) == 1 assert r[0].data.count("END:VEVENT") == 1 + def testRecurringDateWithExceptionSearch(self): + c = self._fixCalendar() + + # evr2 is a bi-weekly event starting 2024-04-11 + e = c.save_event(evr2) + + r = c.search( + start=datetime(2024, 3, 31, 0, 0), + end=datetime(2024, 5, 4, 0, 0, 0), + event=True, + expand=True, + ) + + assert len(r) == 2 + + assert 'RRULE' not in r[0].data + assert 'RRULE' not in r[1].data + + assert isinstance(r[0].icalendar_component['RECURRENCE-ID'], icalendar.vDDDTypes) + assert r[0].icalendar_component['RECURRENCE-ID'].dt == date(2024, 4, 11) + + assert isinstance(r[1].icalendar_component['RECURRENCE-ID'], icalendar.vDDDTypes) + assert r[1].icalendar_component['RECURRENCE-ID'].dt == date(2024, 4, 25) + def testOffsetURL(self): """ pass a URL pointing to a calendar or a user to the DAVClient class,