diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c6b0b37..940285ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 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 +* Fix: calendar-data expand (REPORT) left recurrence properties (e.g. RDATE) on the expanded single-occurrence VEVENTs; a single try/except around the sequential delattr() calls stopped at the first absent property (e.g. missing EXDATE), so later ones were never removed ## 3.7.6 * Extension: item verification on commandline diff --git a/radicale/app/report.py b/radicale/app/report.py index b1f4c3dc..dd41e8bb 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -638,13 +638,15 @@ def _strip_single_event( _convert_to_utc(vevent, 'dtend', dt_format) _convert_to_utc(vevent, 'recurrence_id', dt_format) - try: - delattr(vevent, 'rrule') - delattr(vevent, 'exdate') - delattr(vevent, 'exrule') - delattr(vevent, 'rdate') - except AttributeError: - pass + # Remove every recurrence-defining property independently: a single + # try/except around the sequential delattr() calls stopped at the first + # property that was absent, leaving the following ones (e.g. RDATE) on the + # expanded single-occurrence instance. + for prop in ('rrule', 'exdate', 'exrule', 'rdate'): + try: + delattr(vevent, prop) + except AttributeError: + pass def _strip_component(vevent: vobject.base.Component) -> None: diff --git a/radicale/tests/static/event_rrule_rdate.ics b/radicale/tests/static/event_rrule_rdate.ics new file mode 100644 index 00000000..b3ad7b18 --- /dev/null +++ b/radicale/tests/static/event_rrule_rdate.ics @@ -0,0 +1,12 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Radicale tests//NONSGML test//EN +BEGIN:VEVENT +UID:event_rrule_rdate +DTSTART:20060102T170000Z +DURATION:PT1H +RRULE:FREQ=DAILY;COUNT=3 +RDATE:20060110T170000Z +SUMMARY:Daily recurring meeting +END:VEVENT +END:VCALENDAR diff --git a/radicale/tests/test_expand.py b/radicale/tests/test_expand.py index 807f5f86..d4bf0b72 100644 --- a/radicale/tests/test_expand.py +++ b/radicale/tests/test_expand.py @@ -750,3 +750,36 @@ permissions: RrWw""") ONLY_DATES, 2 ) + + def test_report_with_expand_property_strips_recurrence_properties(self) -> None: + """Expanded instances must not retain recurrence properties. + + Regression test: the stored event has an RRULE and an RDATE but no + EXDATE/EXRULE. Expanded VEVENT instances represent a single + occurrence (identified by RECURRENCE-ID) and must not carry any + recurrence-defining property (RRULE, RDATE, EXRULE, EXDATE). + """ + uid = "event_rrule_rdate" + start = "20060102T000000Z" + end = "20060105T000000Z" + + # Baseline (no expand): the stored event still holds RRULE and RDATE. + _, responses = self.report( + "/calendar.ics/", self._req_without_expand(uid, start, end)) + response = responses[f"/calendar.ics/{uid}.ics"] + assert isinstance(response, dict) + status, element = response["C:calendar-data"] + assert status == 200 and element.text + assert "RRULE" in element.text + assert "RDATE" in element.text + + # Expanded: individual instances must not carry recurrence properties. + _, responses = self.report( + "/calendar.ics/", self._req_with_expand(uid, start, end)) + response = responses[f"/calendar.ics/{uid}.ics"] + assert isinstance(response, dict) + status, element = response["C:calendar-data"] + assert status == 200 and element.text + assert "RECURRENCE-ID" in element.text + assert "RRULE" not in element.text + assert "RDATE" not in element.text