From 2a304259fe28f14d575afeed0bf3b1ecd794dc4e Mon Sep 17 00:00:00 2001 From: Georgiy Date: Fri, 4 Jul 2025 22:47:05 +0300 Subject: [PATCH] (#1812) Processing exdates. Duration handling. Return empty element if all events was filtered Author: Georgiy --- radicale/app/report.py | 56 ++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/radicale/app/report.py b/radicale/app/report.py index 1dba7552..ad176d63 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -358,15 +358,29 @@ def _expand( if hasattr(vevent_recurrence, "dtend"): duration = vevent_recurrence.dtend.value - vevent_recurrence.dtstart.value elif hasattr(vevent_recurrence, "duration"): - duration = vevent_recurrence.duration.value + try: + duration = vevent_recurrence.duration.value + if duration.total_seconds() <= 0: + logger.warning("Invalid DURATION: %s", duration) + duration = None + except (AttributeError, TypeError) as e: + logger.warning("Failed to parse DURATION: %s", e) + duration = None # Generate EXDATE to remove from expansion range + exdates_set = {} if hasattr(vevent_recurrence, 'exdate'): exdates = vevent_recurrence.exdate.value if not isinstance(exdates, list): exdates = [exdates] - logger.debug("EXDATE values: %s", exdates) - # TODO: these exdate values are not removed from the expanded set + + exdates_set = { + exdate.astimezone(datetime.timezone.utc) if isinstance(exdate, datetime.datetime) + else datetime.datetime.fromordinal(exdate.toordinal()).replace(tzinfo=None) + for exdate in exdates + } + + logger.debug("EXDATE values: %s", exdates_set) rruleset = None if hasattr(vevent_recurrence, 'rrule'): @@ -385,8 +399,7 @@ def _expand( # the event. If this introduces an extra reccurence point then # that event should be included as it is still ongoing. If no # extra point is generated then it was a no-op. - - rstart = start - duration if duration else start + rstart = start - duration if duration and duration.total_seconds() > 0 else start recurrences = rruleset.between(rstart, end, inc=True) _strip_component(vevent_component) @@ -407,7 +420,10 @@ def _expand( logger.debug("Recurrence %s filtered out by time-range", recurrence_utc) continue - # Check here for exdate + # Check exdate + if recurrence_utc in exdates_set: + logger.debug("Recurrence %s excluded by EXDATE", recurrence_utc) + continue # Check for overridden instances i_overridden, vevent = _find_overridden(i_overridden, vevents_overridden, recurrence_utc, dt_format) @@ -443,10 +459,6 @@ def _expand( if time_range_start is not None and time_range_end is not None: for vevent in vevents_overridden: dtstart = vevent.dtstart.value - dtend = vevent.dtend.value if hasattr(vevent, 'dtend') else dtstart - logger.debug( - "Filtering VEVENT with DTSTART: %s (type: %s), DTEND: %s (type: %s)", - dtstart, type(dtstart), dtend, type(dtend)) # Handle string values for DTSTART/DTEND if isinstance(dtstart, str): @@ -457,14 +469,12 @@ def _expand( except ValueError as e: logger.warning("Invalid DTSTART format: %s, error: %s", dtstart, e) continue - if isinstance(dtend, str): - try: - dtend = datetime.datetime.strptime(dtend, dt_format) - if all_day_event: - dtend = dtend.date() - except ValueError as e: - logger.warning("Invalid DTEND format: %s, error: %s", dtend, e) - continue + + dtend = dtstart + duration if duration else dtstart + + logger.debug( + "Filtering VEVENT with DTSTART: %s (type: %s), DTEND: %s (type: %s)", + dtstart, type(dtstart), dtend, type(dtend)) # Convert to datetime for comparison if all_day_event and isinstance(dtstart, datetime.date) and not isinstance(dtstart, datetime.datetime): @@ -487,10 +497,14 @@ def _expand( # Rebuild component - # ToDo: Get rid of return vevent_recurrence if filtered_vevents is empty it's wrong behavior - vevent_component.vevent_list = filtered_vevents if filtered_vevents else [vevent_recurrence] + if not filtered_vevents: + element.text = "" + return element + else: + vevent_component.vevent_list = filtered_vevents + logger.debug("lbt: vevent_component %s", vevent_component) + element.text = vevent_component.serialize() - logger.debug("Returning %d VEVENTs", len(vevent_component.vevent_list)) return element