Browse Source

Merge pull request #1849 from pbiering/fix-1847

Fix 1847
Peter Bieringer 6 months ago
parent
commit
19954f162a

+ 1 - 0
CHANGELOG.md

@@ -15,6 +15,7 @@
 * Add: [hook] dryrun: option to disable real hook action for testing, add tests for email+rabbitmq
 * Fix: storage hook path now added to DELETE, MKCOL, MKCALENDAR, MOVE, and PROPPATCH
 * Add: storage hook placeholder now supports "request" and "to_path" (MOVE only)
+* Improve: catch items having tzinfo only on dtstart or dtend set for whatever reason, overtake tzinfo from the other one
 
 ## 3.5.4
 * Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute)

+ 17 - 2
radicale/item/filter.py

@@ -47,7 +47,7 @@ else:
     TRIGGER = datetime | None
 
 
-def date_to_datetime(d: date) -> datetime:
+def date_to_datetime(d: date, tzinfo=vobject.icalendar.utc) -> datetime:
     """Transform any date to a UTC datetime.
 
     If ``d`` is a datetime without timezone, return as UTC datetime. If ``d``
@@ -58,7 +58,7 @@ def date_to_datetime(d: date) -> datetime:
         d = datetime.combine(d, datetime.min.time())
     if not d.tzinfo:
         # NOTE: using vobject's UTC as it wasn't playing well with datetime's.
-        d = d.replace(tzinfo=vobject.icalendar.utc)
+        d = d.replace(tzinfo=tzinfo)
     return d
 
 
@@ -366,6 +366,21 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
             dtend = getattr(child, "dtend", None)
             if dtend is not None:
                 dtend = dtend.value
+
+                # Ensure that both datetime.datetime objects have a timezone or
+                # both do not have one before doing calculations. This is required
+                # as the library does not support performing mathematical operations
+                # on timezone-aware and timezone-naive objects. See #1847
+                if hasattr(dtstart, 'tzinfo') and hasattr(dtend, 'tzinfo'):
+                    if dtstart.tzinfo is None and dtend.tzinfo is not None:
+                        dtstart_orig = dtstart
+                        dtstart = date_to_datetime(dtstart, dtend.astimezone().tzinfo)
+                        logger.debug("TRACE/ITEM/FILTER/get_children: overtake missing tzinfo on dtstart from dtend: '%s' -> '%s'", dtstart_orig, dtstart)
+                    elif dtstart.tzinfo is not None and dtend.tzinfo is None:
+                        dtend_orig = dtend
+                        dtend = date_to_datetime(dtend, dtstart.astimezone().tzinfo)
+                        logger.debug("TRACE/ITEM/FILTER/get_children: overtake missing tzinfo on dtend from dtstart: '%s' -> '%s'", dtend_orig, dtend)
+
                 original_duration = (dtend - dtstart).total_seconds()
                 dtend = date_to_datetime(dtend)
 

+ 14 - 0
radicale/tests/static/event_issue1847_1.ics

@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//algoo.fr//NONSGML Open Calendar v0.9//EN
+BEGIN:VEVENT
+CREATED:20250814T153429Z
+LAST-MODIFIED:20250814T153503Z
+DTSTAMP:20250814T153503Z
+UID:f91964cb-53ca-4942-8811-c38f076f4328
+SUMMARY:error
+DTSTART:20250814T180000
+DTEND;TZID=Europe/Brussels:20250814T190000
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR

+ 14 - 0
radicale/tests/static/event_issue1847_2.ics

@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//algoo.fr//NONSGML Open Calendar v0.9//EN
+BEGIN:VEVENT
+CREATED:20250814T153429Z
+LAST-MODIFIED:20250814T153503Z
+DTSTAMP:20250814T153503Z
+UID:f91964cb-53ca-4942-8811-c38f076f4328
+SUMMARY:error
+DTSTART;TZID=Europe/Brussels:20250814T180000
+DTEND:20250814T190000
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR

+ 16 - 0
radicale/tests/test_base.py

@@ -306,6 +306,22 @@ permissions: RrWw""")
             for uid2 in uids[i + 1:]:
                 assert uid1 != uid2
 
+    def test_add_event_tz_dtend_only(self) -> None:
+        """Add an event having TZ only on DTEND."""
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_issue1847_1.ics")
+        path = "/calendar.ics/event_issue1847_1.ics"
+        self.put(path, event)
+        _, headers, answer = self.request("GET", path, check=200)
+
+    def test_add_event_tz_dtstart_only(self) -> None:
+        """Add an event having TZ only on DTSTART."""
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_issue1847_2.ics")
+        path = "/calendar.ics/event_issue1847_2.ics"
+        self.put(path, event)
+        _, headers, answer = self.request("GET", path, check=200)
+
     def test_verify(self) -> None:
         """Verify the storage."""
         contacts = get_file_content("contact_multiple.vcf")