From b174c345632a43430a45820f8ecfa7f1a20969d2 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Mon, 24 Oct 2022 14:21:31 +0200 Subject: [PATCH 1/2] Support for adding alarms (without having to manually create the icalendar data) to events. Partial fix for https://github.com/python-caldav/caldav/issues/132 --- caldav/lib/vcal.py | 21 +++++++++++++++++++-- caldav/objects.py | 9 +++++---- tests/test_caldav.py | 12 ++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/caldav/lib/vcal.py b/caldav/lib/vcal.py index 088c958c..b2dfbe6f 100644 --- a/caldav/lib/vcal.py +++ b/caldav/lib/vcal.py @@ -81,8 +81,12 @@ def fix(event): ## sorry for being english-language-euro-centric ... fits rather perfectly as default language for me :-) def create_ical(ical_fragment=None, objtype=None, language="en_DK", **props): - """ - I somehow feel this fits more into the icalendar library than here + """Creates some icalendar based on properties given as parameters. + It basically creates an icalendar object with all the boilerplate, + some sensible defaults, the properties given and returns it as a + string. + + TODO: timezones not supported so far """ ical_fragment = to_normal_str(ical_fragment) if not ical_fragment or not re.search("^BEGIN:V", ical_fragment, re.MULTILINE): @@ -105,6 +109,7 @@ def create_ical(ical_fragment=None, objtype=None, language="en_DK", **props): my_instance = icalendar.Calendar.from_ical(ical_fragment) component = my_instance.subcomponents[0] ical_fragment = None + alarm = {} for prop in props: if props[prop] is not None: if prop in ("child", "parent"): @@ -112,11 +117,23 @@ def create_ical(ical_fragment=None, objtype=None, language="en_DK", **props): component.add( "related-to", props[prop], parameters={"rel-type": prop.upper()} ) + elif prop.startswith("alarm_"): + alarm[prop[6:]] = props[prop] else: component.add(prop, props[prop]) + if alarm: + add_alarm(my_instance, alarm) ret = to_normal_str(my_instance.to_ical()) if ical_fragment and ical_fragment.strip(): ret = re.sub( "^END:V", ical_fragment.strip() + "\nEND:V", ret, flags=re.MULTILINE ) return ret + + +def add_alarm(ical, alarm): + ia = icalendar.Alarm() + for prop in alarm: + ia.add(prop, alarm[prop]) + ical.subcomponents[0].add_component(ia) + return ical diff --git a/caldav/objects.py b/caldav/objects.py index ad92a4af..0d1338a7 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -704,7 +704,8 @@ def save_event(self, ical=None, no_overwrite=False, no_create=False, **ical_data * ical - ical object (text) * no_overwrite - existing calendar objects should not be overwritten * no_create - don't create a new object, existing calendar objects should be updated - * ical_data - passed to lib.vcal.create_ical + * dt_start, dt_end, summary, etc - properties to be inserted into the icalendar object + * alarm_trigger, alarm_action, alarm_attach, etc - when given, one alarm will be added """ e = Event( self.client, @@ -1906,7 +1907,7 @@ def copy(self, keep_uid=False, new_parent=None): id=self.id if keep_uid else str(uuid.uuid1()), ) if new_parent or not keep_uid: - obj.url = obj.generate_url() + obj.url = obj._generate_url() else: obj.url = self.url return obj @@ -1963,7 +1964,7 @@ def _find_id_path(self, id=None, path=None): error.assert_(x.get("UID", None) == self.id) if path is None: - path = self.generate_url() + path = self._generate_url() else: path = self.parent.url.join(path) @@ -1992,7 +1993,7 @@ def _create(self, id=None, path=None, retry_on_failure=True): self._find_id_path(id=id, path=path) self._put() - def generate_url(self): + def _generate_url(self): ## See https://github.com/python-caldav/caldav/issues/143 for the rationale behind double-quoting slashes ## TODO: should try to wrap my head around issues that arises when id contains weird characters. maybe it's ## better to generate a new uuid here, particularly if id is in some unexpected format. diff --git a/tests/test_caldav.py b/tests/test_caldav.py index 5586ad68..b46ec05c 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -17,6 +17,7 @@ from collections import namedtuple from datetime import date from datetime import datetime +from datetime import timedelta import pytest import requests @@ -766,6 +767,17 @@ def testCreateEvent(self): events = c.events() assert len(events) == len(existing_events) + 2 + def testCreateAlarm(self): + c = self._fixCalendar() + ev = c.save_event( + dtstart=datetime(2015, 10, 10, 8, 7, 6), + summary="This is a test event", + dtend=datetime(2016, 10, 10, 9, 8, 7), + alarm_trigger=timedelta(minutes=-15), + alarm_action="AUDIO", + ) + pass + def testCalendarByFullURL(self): """ ref private email, passing a full URL as cal_id works in 0.5.0 but From 9f2e6e0142869281ca337f878d7de511c71b0502 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Mon, 24 Oct 2022 15:41:28 +0200 Subject: [PATCH 2/2] search for alarms, with test code --- caldav/objects.py | 7 +++++++ tests/test_caldav.py | 26 ++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/caldav/objects.py b/caldav/objects.py index 0d1338a7..bb626b1e 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -1107,6 +1107,8 @@ def build_search_xml_query( expand=None, start=None, end=None, + alarm_start=None, + alarm_end=None, **kwargs ): """This method will produce a caldav search query as an etree object. @@ -1165,6 +1167,11 @@ def build_search_xml_query( if start or end: filters.append(cdav.TimeRange(start, end)) + if alarm_start or alarm_end: + filters.append( + cdav.CompFilter("VALARM") + cdav.TimeRange(alarm_start, alarm_end) + ) + if todo is not None: if not todo: raise NotImplementedError() diff --git a/tests/test_caldav.py b/tests/test_caldav.py index b46ec05c..386eddcf 100644 --- a/tests/test_caldav.py +++ b/tests/test_caldav.py @@ -767,7 +767,8 @@ def testCreateEvent(self): events = c.events() assert len(events) == len(existing_events) + 2 - def testCreateAlarm(self): + def testAlarm(self): + ## Ref https://github.com/python-caldav/caldav/issues/132 c = self._fixCalendar() ev = c.save_event( dtstart=datetime(2015, 10, 10, 8, 7, 6), @@ -776,7 +777,28 @@ def testCreateAlarm(self): alarm_trigger=timedelta(minutes=-15), alarm_action="AUDIO", ) - pass + + ## Search for the alarm (procrastinated - see https://github.com/python-caldav/caldav/issues/132) + assert ( + len( + c.search( + event=True, + alarm_start=datetime(2015, 10, 10, 8, 1), + alarm_end=datetime(2015, 10, 10, 8, 7), + ) + ) + == 0 + ) + assert ( + len( + c.search( + event=True, + alarm_start=datetime(2015, 10, 10, 7, 44), + alarm_end=datetime(2015, 10, 10, 8, 7), + ) + ) + == 1 + ) def testCalendarByFullURL(self): """