mirror of
https://github.com/Kozea/Radicale.git
synced 2025-08-04 18:22:26 +00:00
(#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
This commit is contained in:
parent
01bcc7d009
commit
097360139a
1 changed files with 68 additions and 42 deletions
|
@ -332,6 +332,7 @@ def _expand(
|
||||||
time_range_end: Optional[datetime.datetime] = None,
|
time_range_end: Optional[datetime.datetime] = None,
|
||||||
) -> ET.Element:
|
) -> ET.Element:
|
||||||
vevent_component: vobject.base.Component = copy.copy(item.vobject_item)
|
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
|
# Split the vevents included in the component into one that contains the
|
||||||
# recurrence information and others that contain a recurrence id to
|
# recurrence information and others that contain a recurrence id to
|
||||||
|
@ -350,6 +351,9 @@ def _expand(
|
||||||
# rruleset.between computes with datetimes without timezone information
|
# rruleset.between computes with datetimes without timezone information
|
||||||
start = start.replace(tzinfo=None)
|
start = start.replace(tzinfo=None)
|
||||||
end = end.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:
|
for vevent in vevents_overridden:
|
||||||
_strip_single_event(vevent, dt_format)
|
_strip_single_event(vevent, dt_format)
|
||||||
|
@ -358,10 +362,21 @@ def _expand(
|
||||||
if hasattr(vevent_recurrence, "dtend"):
|
if hasattr(vevent_recurrence, "dtend"):
|
||||||
duration = vevent_recurrence.dtend.value - vevent_recurrence.dtstart.value
|
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
|
rruleset = None
|
||||||
if hasattr(vevent_recurrence, 'rrule'):
|
if hasattr(vevent_recurrence, 'rrule'):
|
||||||
rruleset = vevent_recurrence.getrruleset()
|
rruleset = vevent_recurrence.getrruleset()
|
||||||
|
|
||||||
|
filtered_vevents = []
|
||||||
if rruleset:
|
if rruleset:
|
||||||
# This function uses datetimes internally without timezone info for dates
|
# This function uses datetimes internally without timezone info for dates
|
||||||
recurrences = rruleset.between(start, end, inc=True)
|
recurrences = rruleset.between(start, end, inc=True)
|
||||||
|
@ -369,27 +384,32 @@ def _expand(
|
||||||
_strip_component(vevent_component)
|
_strip_component(vevent_component)
|
||||||
_strip_single_event(vevent_recurrence, dt_format)
|
_strip_single_event(vevent_recurrence, dt_format)
|
||||||
|
|
||||||
is_component_filled: bool = False
|
|
||||||
i_overridden = 0
|
i_overridden = 0
|
||||||
|
|
||||||
for recurrence_dt in recurrences:
|
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:
|
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
|
dtend = dtstart + duration if duration else dtstart
|
||||||
if not (dtstart < time_range_end and dtend > time_range_start):
|
if not (dtstart < time_range_end and dtend > time_range_start):
|
||||||
|
logger.debug("Recurrence %s filtered out by time-range", recurrence_utc)
|
||||||
continue
|
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)
|
||||||
|
|
||||||
if not vevent:
|
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)
|
vevent = copy.deepcopy(vevent_recurrence)
|
||||||
|
|
||||||
# For all day events, the system timezone may influence the
|
# For all day events, the system timezone may influence the
|
||||||
# results, so use recurrence_dt
|
# results, so use recurrence_dt
|
||||||
recurrence_id = recurrence_dt if all_day_event else recurrence_utc
|
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(
|
vevent.recurrence_id = ContentLine(
|
||||||
name='RECURRENCE-ID',
|
name='RECURRENCE-ID',
|
||||||
value=recurrence_id, params={}
|
value=recurrence_id, params={}
|
||||||
|
@ -405,54 +425,60 @@ def _expand(
|
||||||
value=(recurrence_id + duration).strftime(dt_format), params={}
|
value=(recurrence_id + duration).strftime(dt_format), params={}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not is_component_filled:
|
filtered_vevents.append(vevent)
|
||||||
vevent_component.vevent = vevent
|
|
||||||
is_component_filled = True
|
|
||||||
else:
|
|
||||||
vevent_component.add(vevent)
|
|
||||||
|
|
||||||
# Filter overridden events and vevent_recurrence if recurrences is empty
|
# Filter overridden and recurrence base events
|
||||||
# Todo: optimize that code
|
|
||||||
if time_range_start is not None and time_range_end is not None:
|
if time_range_start is not None and time_range_end is not None:
|
||||||
filtered_vevents = []
|
for vevent in vevents_overridden + [vevent_recurrence]:
|
||||||
for vevent in vevents_overridden:
|
|
||||||
dtstart = vevent.dtstart.value
|
dtstart = vevent.dtstart.value
|
||||||
dtend = vevent.dtend.value if hasattr(vevent, 'dtend') else dtstart
|
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))
|
||||||
|
|
||||||
dtstart = datetime.datetime.strptime(
|
# Handle string values for DTSTART/DTEND
|
||||||
dtstart, "%Y%m%dT%H%M%SZ").replace(
|
if isinstance(dtstart, str):
|
||||||
tzinfo=datetime.timezone.utc)
|
try:
|
||||||
dtend = datetime.datetime.strptime(
|
dtstart = datetime.datetime.strptime(dtstart, dt_format)
|
||||||
dtend, "%Y%m%dT%H%M%SZ").replace(
|
if all_day_event:
|
||||||
tzinfo=datetime.timezone.utc)
|
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
|
||||||
|
|
||||||
if dtstart < time_range_end and dtend > time_range_start:
|
if dtstart < time_range_end and dtend > time_range_start:
|
||||||
filtered_vevents.append(vevent)
|
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)
|
||||||
|
|
||||||
dtstart = vevent_recurrence.dtstart.value
|
# Rebuild component
|
||||||
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
|
|
||||||
|
|
||||||
|
# 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()
|
element.text = vevent_component.serialize()
|
||||||
|
logger.debug("Returning %d VEVENTs", len(vevent_component.vevent_list))
|
||||||
|
|
||||||
return element
|
return element
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue