From 859ca7f41d6522d4448037779d395b2d9c1ae7b7 Mon Sep 17 00:00:00 2001 From: Georgiy Date: Tue, 3 Feb 2026 19:39:24 +0300 Subject: [PATCH 1/2] (#1979) Fix invalid RECURRENCE-ID comparsion for all-day events --- radicale/app/report.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index 752d04a7..7b5b01c0 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -471,7 +471,13 @@ def _expand( continue # Check for overridden instances - i_overridden, vevent = _find_overridden(i_overridden, vevents_overridden, recurrence_utc, dt_format) + i_overridden, vevent = _find_overridden( + i_overridden, + vevents_overridden, + recurrence_utc, + dt_format, + all_day_event, + ) if not vevent: # Create new instance from recurrence @@ -649,13 +655,13 @@ def _find_overridden( start: int, vevents: List[vobject.icalendar.RecurringComponent], dt: datetime.datetime, - dt_format: str + dt_format: str, + all_day_event: bool, ) -> Tuple[int, Optional[vobject.icalendar.RecurringComponent]]: for i in range(start, len(vevents)): - dt_event = datetime.datetime.strptime( - vevents[i].recurrence_id.value, - dt_format - ).replace(tzinfo=datetime.timezone.utc) + dt_event = datetime.datetime.strptime(vevents[i].recurrence_id.value, dt_format) + if not all_day_event: + dt_event = dt_event.replace(tzinfo=datetime.timezone.utc) if dt_event == dt: return (i + 1, vevents[i]) return (start, None) From 76d798ccd596c21fded05af9fd1b5b3846a712ab Mon Sep 17 00:00:00 2001 From: Georgiy Date: Wed, 4 Feb 2026 19:26:12 +0300 Subject: [PATCH 2/2] (#1979) test for overridden all day rrule event --- radicale/app/report.py | 29 ++++++++++++------- .../event_full_day_rrule_overridden.ics | 21 ++++++++++++++ radicale/tests/test_expand.py | 12 ++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 radicale/tests/static/event_full_day_rrule_overridden.ics diff --git a/radicale/app/report.py b/radicale/app/report.py index 7b5b01c0..5652788e 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -386,7 +386,7 @@ def _expand( time_range_end = time_range_end.replace(tzinfo=None) for vevent in vevents_overridden: - _strip_single_event(vevent, dt_format) + _strip_single_event(vevent, dt_format, all_day_event) duration = None if hasattr(base_vevent, "dtend"): @@ -448,7 +448,7 @@ def _expand( .format(max_occurrence)) _strip_component(vevent_component) - _strip_single_event(base_vevent, dt_format) + _strip_single_event(base_vevent, dt_format, all_day_event) i_overridden = 0 @@ -565,15 +565,20 @@ def _expand( return element, len(filtered_vevents) -def _convert_timezone(vevent: vobject.icalendar.RecurringComponent, - name_prop: str, - name_content_line: str): +def _convert_timezone( + vevent: vobject.icalendar.RecurringComponent, + name_prop: str, + name_content_line: str, + all_day_event: bool = False +): prop = getattr(vevent, name_prop, None) if prop: if type(prop.value) is datetime.date: date_time = datetime.datetime.fromordinal( prop.value.toordinal() - ).replace(tzinfo=datetime.timezone.utc) + ) + if not all_day_event: + date_time = date_time.replace(tzinfo=datetime.timezone.utc) else: date_time = prop.value.astimezone(datetime.timezone.utc) @@ -588,10 +593,14 @@ def _convert_to_utc(vevent: vobject.icalendar.RecurringComponent, setattr(vevent, name_prop, ContentLine(name=prop.name, value=prop.value.strftime(dt_format), params=[])) -def _strip_single_event(vevent: vobject.icalendar.RecurringComponent, dt_format: str) -> None: - _convert_timezone(vevent, 'dtstart', 'DTSTART') - _convert_timezone(vevent, 'dtend', 'DTEND') - _convert_timezone(vevent, 'recurrence_id', 'RECURRENCE-ID') +def _strip_single_event( + vevent: vobject.icalendar.RecurringComponent, + dt_format: str, + all_day_event: bool = False, +) -> None: + _convert_timezone(vevent, 'dtstart', 'DTSTART', all_day_event) + _convert_timezone(vevent, 'dtend', 'DTEND', all_day_event) + _convert_timezone(vevent, 'recurrence_id', 'RECURRENCE-ID', all_day_event) # There is something strange behaviour during serialization native datetime, so converting manually _convert_to_utc(vevent, 'dtstart', dt_format) diff --git a/radicale/tests/static/event_full_day_rrule_overridden.ics b/radicale/tests/static/event_full_day_rrule_overridden.ics new file mode 100644 index 00000000..d79346b7 --- /dev/null +++ b/radicale/tests/static/event_full_day_rrule_overridden.ics @@ -0,0 +1,21 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Radicale//Test//EN + +BEGIN:VEVENT +DTSTART;VALUE=DATE:20060102 +DTEND;VALUE=DATE:20060103 +RRULE:FREQ=DAILY;COUNT=3 +UID:event_full_day_rrule_overridden +SUMMARY:All day recurring +END:VEVENT + +BEGIN:VEVENT +DTSTART;VALUE=DATE:20060110 +DTEND;VALUE=DATE:20060111 +RECURRENCE-ID;VALUE=DATE:20060103 +UID:event_full_day_rrule_overridden +SUMMARY:Overridden instance +END:VEVENT + +END:VCALENDAR \ No newline at end of file diff --git a/radicale/tests/test_expand.py b/radicale/tests/test_expand.py index 2cc4a49f..09e437a7 100644 --- a/radicale/tests/test_expand.py +++ b/radicale/tests/test_expand.py @@ -512,3 +512,15 @@ permissions: RrWw""") status, event2_calendar_data = responses["/test/event2.ics"]["C:calendar-data"] assert event2_calendar_data.text assert "UID:c6be8b2c-3d72-453c-b698-4f25cdf1569e" in event2_calendar_data.text + + def test_report_with_expand_property_all_day_event_overridden(self) -> None: + self._test_expand( + "event_full_day_rrule_overridden", + "20060101T000000Z", + "20060111T235959Z", + ["RECURRENCE-ID:20060102", "RECURRENCE-ID:20060104", "RECURRENCE-ID:20060103"], + ["DTSTART:20060102", "DTSTART:20060104", "DTSTART:20060110"], + ["DTEND:20060103", "DTEND:20060105", "DTEND:20060111"], + ONLY_DATES, + 2 + )