Ver código fonte

(#1812) Refactored _expand function to process overridden VEVENTs and
support EXDATE for recurrence filtering. Added temporary workaround to
return base vevent_recurrence when filtered_vevents is empty to avoid
'list index out of range' error

Georgiy 8 meses atrás
pai
commit
097360139a
1 arquivos alterados com 70 adições e 44 exclusões
  1. 70 44
      radicale/app/report.py

+ 70 - 44
radicale/app/report.py

@@ -332,6 +332,7 @@ def _expand(
         time_range_end: Optional[datetime.datetime] = None,
 ) -> ET.Element:
     vevent_component: vobject.base.Component = copy.copy(item.vobject_item)
+    logger.info("Expanding event %s", item.href)
 
     # Split the vevents included in the component into one that contains the
     # recurrence information and others that contain a recurrence id to
@@ -350,6 +351,9 @@ def _expand(
         # rruleset.between computes with datetimes without timezone information
         start = start.replace(tzinfo=None)
         end = end.replace(tzinfo=None)
+        if time_range_start is not None and time_range_end is not None:
+            time_range_start = time_range_start.replace(tzinfo=None)
+            time_range_end = time_range_end.replace(tzinfo=None)
 
     for vevent in vevents_overridden:
         _strip_single_event(vevent, dt_format)
@@ -358,10 +362,21 @@ def _expand(
     if hasattr(vevent_recurrence, "dtend"):
         duration = vevent_recurrence.dtend.value - vevent_recurrence.dtstart.value
 
+    # Handle EXDATE to limit expansion range
+    if hasattr(vevent_recurrence, 'exdate'):
+        exdates = vevent_recurrence.exdate.value
+        if not isinstance(exdates, list):
+            exdates = [exdates]  # Convert single date to list
+        logger.debug("EXDATE values: %s", exdates)
+        latest_exdate = max(exdates) if exdates else None
+        if latest_exdate and end > latest_exdate:
+            end = min(end, latest_exdate)
+
     rruleset = None
     if hasattr(vevent_recurrence, 'rrule'):
         rruleset = vevent_recurrence.getrruleset()
 
+    filtered_vevents = []
     if rruleset:
         # This function uses datetimes internally without timezone info for dates
         recurrences = rruleset.between(start, end, inc=True)
@@ -369,27 +384,32 @@ def _expand(
         _strip_component(vevent_component)
         _strip_single_event(vevent_recurrence, dt_format)
 
-        is_component_filled: bool = False
         i_overridden = 0
 
         for recurrence_dt in recurrences:
-            recurrence_utc = recurrence_dt.astimezone(datetime.timezone.utc)
+            recurrence_utc = recurrence_dt if all_day_event else recurrence_dt.astimezone(datetime.timezone.utc)
+            logger.debug("Processing recurrence: %s (all_day_event: %s)", recurrence_utc, all_day_event)
 
+            # Apply time-range filter
             if time_range_start is not None and time_range_end is not None:
-                dtstart = recurrence_dt if all_day_event else recurrence_utc
+                dtstart = recurrence_utc
                 dtend = dtstart + duration if duration else dtstart
                 if not (dtstart < time_range_end and dtend > time_range_start):
+                    logger.debug("Recurrence %s filtered out by time-range", recurrence_utc)
                     continue
 
+            # Check for overridden instances
             i_overridden, vevent = _find_overridden(i_overridden, vevents_overridden, recurrence_utc, dt_format)
 
             if not vevent:
-                # We did not find an overridden instance, so create a new one
+                # Create new instance from recurrence
                 vevent = copy.deepcopy(vevent_recurrence)
 
                 # For all day events, the system timezone may influence the
                 # results, so use recurrence_dt
                 recurrence_id = recurrence_dt if all_day_event else recurrence_utc
+                logger.debug("Creating new VEVENT with RECURRENCE-ID: %s", recurrence_id)
+
                 vevent.recurrence_id = ContentLine(
                     name='RECURRENCE-ID',
                     value=recurrence_id, params={}
@@ -405,54 +425,60 @@ def _expand(
                         value=(recurrence_id + duration).strftime(dt_format), params={}
                     )
 
-            if not is_component_filled:
-                vevent_component.vevent = vevent
-                is_component_filled = True
-            else:
-                vevent_component.add(vevent)
+            filtered_vevents.append(vevent)
 
-        # Filter overridden events and vevent_recurrence if recurrences is empty
-        # Todo: optimize that code
+        # Filter overridden and recurrence base events
         if time_range_start is not None and time_range_end is not None:
-            filtered_vevents = []
-            for vevent in vevents_overridden:
+            for vevent in vevents_overridden + [vevent_recurrence]:
                 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):
+                    try:
+                        dtstart = datetime.datetime.strptime(dtstart, dt_format)
+                        if all_day_event:
+                            dtstart = dtstart.date()
+                    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 DTSTART format: %s, error: %s", dtstart, e)
+                        continue
+
+                # Convert to datetime for comparison
+                if all_day_event and isinstance(dtstart, datetime.date) and not isinstance(dtstart, datetime.datetime):
+                    dtstart = datetime.datetime.fromordinal(dtstart.toordinal()).replace(tzinfo=None)
+                    dtend = datetime.datetime.fromordinal(dtend.toordinal()).replace(tzinfo=None)
+                elif not all_day_event and isinstance(dtstart, datetime.datetime) \
+                        and isinstance(dtend, datetime.datetime):
+                    dtstart = dtstart.replace(tzinfo=datetime.timezone.utc)
+                    dtend = dtend.replace(tzinfo=datetime.timezone.utc)
+                else:
+                    logger.warning("Unexpected DTSTART/DTEND type: dtstart=%s, dtend=%s", type(dtstart), type(dtend))
+                    continue
 
-                dtstart = datetime.datetime.strptime(
-                    dtstart, "%Y%m%dT%H%M%SZ").replace(
-                        tzinfo=datetime.timezone.utc)
-                dtend = datetime.datetime.strptime(
-                    dtend, "%Y%m%dT%H%M%SZ").replace(
-                        tzinfo=datetime.timezone.utc)
-
-                if dtstart < time_range_end and dtend > time_range_start:
-                    filtered_vevents.append(vevent)
-
-            dtstart = vevent_recurrence.dtstart.value
-            dtend = vevent_recurrence.dtend.value if hasattr(vevent_recurrence, 'dtend') else dtstart
-            dtstart = datetime.datetime.strptime(
-                dtstart, "%Y%m%dT%H%M%SZ").replace(
-                    tzinfo=datetime.timezone.utc)
-            dtend = datetime.datetime.strptime(
-                dtend, "%Y%m%dT%H%M%SZ").replace(
-                    tzinfo=datetime.timezone.utc)
-
-            if filtered_vevents or (dtstart < time_range_end and dtend > time_range_start):
-                if filtered_vevents:
-                    vevent_component.vevent = filtered_vevents[0]
-                    for vevent in filtered_vevents[1:]:
-                        vevent_component.add(vevent)
                 if dtstart < time_range_end and dtend > time_range_start:
-                    if not filtered_vevents:
-                        vevent_component.vevent = vevent_recurrence
-                    else:
-                        vevent_component.add(vevent_recurrence)
-            else:
-                element.text = ""
-                return element
+                    if vevent not in filtered_vevents:  # Avoid duplicates
+                        logger.debug("VEVENT passed time-range filter: %s", dtstart)
+                        filtered_vevents.append(vevent)
+                else:
+                    logger.debug("VEVENT filtered out: %s", dtstart)
+
+    # 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]
     element.text = vevent_component.serialize()
+    logger.debug("Returning %d VEVENTs", len(vevent_component.vevent_list))
 
     return element