|
21 | 21 |
|
22 | 22 | import logging |
23 | 23 | from collections.abc import Iterable |
24 | | -from datetime import datetime, time, timedelta, timezone |
| 24 | +from datetime import date, datetime, time, timedelta, timezone |
25 | 25 | from typing import Callable, Optional, Union |
26 | 26 | from zoneinfo import ZoneInfo |
27 | 27 |
|
28 | 28 | import dateutil.rrule |
29 | 29 | from icalendar.cal import Calendar, Component, component_factory |
30 | | -from icalendar.prop import TypesFactory, vCategory, vDatetime, vDDDTypes, vText |
| 30 | +from icalendar.prop import TypesFactory, vCategory, vDate, vDatetime, vDDDTypes, vText |
31 | 31 |
|
32 | 32 | from xandikos.store import File, Filter, InvalidFileContents |
33 | 33 |
|
@@ -974,19 +974,34 @@ def _expand_rrule_component( |
974 | 974 | if "RRULE" not in incomp: |
975 | 975 | return |
976 | 976 | rs = rruleset_from_comp(incomp) |
| 977 | + |
| 978 | + # Check if original DTSTART is date-only |
| 979 | + original_dtstart = incomp["DTSTART"] |
| 980 | + is_date_only = isinstance(original_dtstart.dt, date) and not isinstance( |
| 981 | + original_dtstart.dt, datetime |
| 982 | + ) |
| 983 | + |
977 | 984 | for field in ["RRULE", "EXRULE", "UNTIL", "RDATE", "EXDATE"]: |
978 | 985 | if field in incomp: |
979 | 986 | del incomp[field] |
980 | 987 | # Work our magic |
981 | | - for ts in rs.between(start, end): |
982 | | - utcts = asutc(ts) |
| 988 | + for ts in rs.between(start, end, inc=True): |
| 989 | + # For date-only events, convert rrule's datetime back to date |
| 990 | + if is_date_only: |
| 991 | + ts_normalized = ts.date() |
| 992 | + else: |
| 993 | + ts_normalized = asutc(ts) |
| 994 | + |
983 | 995 | try: |
984 | | - outcomp = existing.pop(utcts) |
985 | | - outcomp["DTSTART"] = vDatetime(asutc(outcomp["DTSTART"].dt)) |
| 996 | + outcomp = existing.pop(ts_normalized) |
| 997 | + # Preserve the original DTSTART value and type from the exception |
| 998 | + # It's already the correct type |
986 | 999 | except KeyError: |
987 | 1000 | outcomp = incomp.copy() |
988 | | - outcomp["DTSTART"] = vDatetime(utcts) |
989 | | - outcomp["RECURRENCE-ID"] = vDatetime(utcts) |
| 1001 | + outcomp["DTSTART"] = create_prop_from_date_or_datetime(ts_normalized) |
| 1002 | + |
| 1003 | + # Set RECURRENCE-ID with appropriate type |
| 1004 | + outcomp["RECURRENCE-ID"] = create_prop_from_date_or_datetime(ts_normalized) |
990 | 1005 | yield outcomp |
991 | 1006 |
|
992 | 1007 |
|
@@ -1016,4 +1031,15 @@ def expand_calendar_rrule(incal: Calendar, start: datetime, end: datetime) -> Ca |
1016 | 1031 |
|
1017 | 1032 |
|
1018 | 1033 | def asutc(dt): |
| 1034 | + if isinstance(dt, date) and not isinstance(dt, datetime): |
| 1035 | + # Return date as-is - dates are timezone-agnostic |
| 1036 | + return dt |
1019 | 1037 | return dt.astimezone(ZoneInfo("UTC")).replace(tzinfo=None) |
| 1038 | + |
| 1039 | + |
| 1040 | +def create_prop_from_date_or_datetime(dt): |
| 1041 | + """Create appropriate vDate or vDatetime property based on input type.""" |
| 1042 | + if isinstance(dt, date) and not isinstance(dt, datetime): |
| 1043 | + return vDate(dt) |
| 1044 | + else: |
| 1045 | + return vDatetime(dt) |
0 commit comments