mirror of
https://github.com/Kozea/Radicale.git
synced 2025-08-01 18:18:31 +00:00
(#1812) Fixed an issue where non-recurring events were not included in
the response when requesting an expand report
This commit is contained in:
parent
c553460365
commit
7cd918d036
4 changed files with 120 additions and 50 deletions
|
@ -360,12 +360,12 @@ def _expand(
|
|||
# Split the vevents included in the component into one that contains the
|
||||
# recurrence information and others that contain a recurrence id to
|
||||
# override instances.
|
||||
vevent_recurrence, vevents_overridden = _split_overridden_vevents(vevent_component)
|
||||
base_vevent, vevents_overridden = _split_overridden_vevents(vevent_component)
|
||||
|
||||
dt_format = '%Y%m%dT%H%M%SZ'
|
||||
all_day_event = False
|
||||
|
||||
if type(vevent_recurrence.dtstart.value) is datetime.date:
|
||||
if type(base_vevent.dtstart.value) is datetime.date:
|
||||
# If an event comes to us with a dtstart specified as a date
|
||||
# then in the response we return the date, not datetime
|
||||
dt_format = '%Y%m%d'
|
||||
|
@ -382,11 +382,11 @@ def _expand(
|
|||
_strip_single_event(vevent, dt_format)
|
||||
|
||||
duration = None
|
||||
if hasattr(vevent_recurrence, "dtend"):
|
||||
duration = vevent_recurrence.dtend.value - vevent_recurrence.dtstart.value
|
||||
elif hasattr(vevent_recurrence, "duration"):
|
||||
if hasattr(base_vevent, "dtend"):
|
||||
duration = base_vevent.dtend.value - base_vevent.dtstart.value
|
||||
elif hasattr(base_vevent, "duration"):
|
||||
try:
|
||||
duration = vevent_recurrence.duration.value
|
||||
duration = base_vevent.duration.value
|
||||
if duration.total_seconds() <= 0:
|
||||
logger.warning("Invalid DURATION: %s", duration)
|
||||
duration = None
|
||||
|
@ -396,8 +396,8 @@ def _expand(
|
|||
|
||||
# Generate EXDATE to remove from expansion range
|
||||
exdates_set: set[datetime.datetime] = set()
|
||||
if hasattr(vevent_recurrence, 'exdate'):
|
||||
exdates = vevent_recurrence.exdate.value
|
||||
if hasattr(base_vevent, 'exdate'):
|
||||
exdates = base_vevent.exdate.value
|
||||
if not isinstance(exdates, list):
|
||||
exdates = [exdates]
|
||||
|
||||
|
@ -409,9 +409,14 @@ def _expand(
|
|||
|
||||
logger.debug("EXDATE values: %s", exdates_set)
|
||||
|
||||
events_for_filtering = vevents_overridden
|
||||
|
||||
rruleset = None
|
||||
if hasattr(vevent_recurrence, 'rrule'):
|
||||
rruleset = vevent_recurrence.getrruleset()
|
||||
if hasattr(base_vevent, 'rrule'):
|
||||
rruleset = base_vevent.getrruleset()
|
||||
else:
|
||||
# if event does not have rrule, only include base event
|
||||
events_for_filtering = [base_vevent]
|
||||
|
||||
filtered_vevents = []
|
||||
if rruleset:
|
||||
|
@ -436,7 +441,7 @@ def _expand(
|
|||
.format(max_occurrence))
|
||||
|
||||
_strip_component(vevent_component)
|
||||
_strip_single_event(vevent_recurrence, dt_format)
|
||||
_strip_single_event(base_vevent, dt_format)
|
||||
|
||||
i_overridden = 0
|
||||
|
||||
|
@ -463,7 +468,7 @@ def _expand(
|
|||
|
||||
if not vevent:
|
||||
# Create new instance from recurrence
|
||||
vevent = copy.deepcopy(vevent_recurrence)
|
||||
vevent = copy.deepcopy(base_vevent)
|
||||
|
||||
# For all day events, the system timezone may influence the
|
||||
# results, so use recurrence_dt
|
||||
|
@ -488,45 +493,45 @@ def _expand(
|
|||
|
||||
filtered_vevents.append(vevent)
|
||||
|
||||
# Filter overridden and recurrence base events
|
||||
if time_range_start is not None and time_range_end is not None:
|
||||
for vevent in vevents_overridden:
|
||||
dtstart = vevent.dtstart.value
|
||||
# Filter overridden and non-recurring events
|
||||
if time_range_start is not None and time_range_end is not None:
|
||||
for vevent in events_for_filtering:
|
||||
dtstart = vevent.dtstart.value
|
||||
|
||||
# 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
|
||||
|
||||
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):
|
||||
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))
|
||||
# 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 dtstart < time_range_end and dtend > time_range_start:
|
||||
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)
|
||||
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):
|
||||
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 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
|
||||
|
||||
|
|
31
radicale/tests/static/event_issue1812_2.ics
Normal file
31
radicale/tests/static/event_issue1812_2.ics
Normal file
|
@ -0,0 +1,31 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//algoo.fr//NONSGML Open Calendar v0.9//EN
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Paris
|
||||
LAST-MODIFIED:20250523T094234Z
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19701025T030000Z
|
||||
RRULE:BYDAY=-1SU;BYMONTH=10;FREQ=YEARLY
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:19700329T020000Z
|
||||
RRULE:BYDAY=-1SU;BYMONTH=3;FREQ=YEARLY
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:a07cfa8b-0ce6-4956-800d-c0bfe1f0730a
|
||||
DTSTART;TZID=Europe/Paris;VALUE=DATE:20250716
|
||||
DTEND;TZID=Europe/Paris;VALUE=DATE:20250718
|
||||
DTSTAMP;VALUE=DATE-TIME:20250721T075355Z
|
||||
RRULE:FREQ=WEEKLY
|
||||
SEQUENCE:1
|
||||
SUMMARY:bla
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
31
radicale/tests/static/event_issue1812_3.ics
Normal file
31
radicale/tests/static/event_issue1812_3.ics
Normal file
|
@ -0,0 +1,31 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//algoo.fr//NONSGML Open Calendar v0.9//EN
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Paris
|
||||
LAST-MODIFIED:20250523T094234Z
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19701025T030000Z
|
||||
RRULE:BYDAY=-1SU;BYMONTH=10;FREQ=YEARLY
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:19700329T020000Z
|
||||
RRULE:BYDAY=-1SU;BYMONTH=3;FREQ=YEARLY
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:c6be8b2c-3d72-453c-b698-4f25cdf1569e
|
||||
DTSTART;TZID=Europe/Paris;VALUE=DATE-TIME:20250716T110000
|
||||
DTEND;TZID=Europe/Paris;VALUE=DATE-TIME:20250716T120000
|
||||
ATTENDEE;CN=Corentin;ROLE=REQ-PARTICIPANT:MAILTO:corentin.jeanne@algoo.fr
|
||||
DTSTAMP:20250718T151312Z
|
||||
ORGANIZER;CN=Sigma:MAILTO:lambda@lambda.lambda
|
||||
SUMMARY:Test mail notifications 2
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -474,8 +474,8 @@ permissions: RrWw""")
|
|||
self.put("/test/event.ics/", get_file_content("event_issue1812_2.ics"))
|
||||
self.put("/test/event2.ics/", get_file_content("event_issue1812_3.ics"))
|
||||
|
||||
request = f"""
|
||||
<c:calendar-query xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/" xmlns:ca="http://apple.com/ns/ical/" xmlns:d="DAV:">
|
||||
request = """
|
||||
<c:calendar-query xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:">
|
||||
<d:prop>
|
||||
<d:getetag/>
|
||||
<c:calendar-data>
|
||||
|
@ -504,6 +504,9 @@ permissions: RrWw""")
|
|||
status, event1_calendar_data = responses["/test/event.ics"]["C:calendar-data"]
|
||||
assert event1_calendar_data.text
|
||||
assert "UID:a07cfa8b-0ce6-4956-800d-c0bfe1f0730a" in event1_calendar_data.text
|
||||
assert "RECURRENCE-ID:20250716" in event1_calendar_data.text
|
||||
assert "RECURRENCE-ID:20250723" in event1_calendar_data.text
|
||||
assert "RECURRENCE-ID:20250730" in event1_calendar_data.text
|
||||
|
||||
assert "C:calendar-data" in responses["/test/event2.ics"]
|
||||
status, event2_calendar_data = responses["/test/event2.ics"]["C:calendar-data"]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue