From 4a5b2195a752dcc400ba81a7280f410c1c7be966 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Sat, 26 Nov 2022 23:51:38 +0100 Subject: [PATCH 1/2] we need get_duration for events in the calendar-cli project --- caldav/objects.py | 74 ++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/caldav/objects.py b/caldav/objects.py index 6480ddef..fe180175 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -1618,6 +1618,8 @@ class CalendarObjectResource(DAVObject): event, a todo-item, a journal entry, or a free/busy entry """ + _ENDPARAM = None + _vobject_instance = None _icalendar_instance = None _data = None @@ -2223,6 +2225,40 @@ def _get_icalendar_instance(self): doc="icalendar instance of the object", ) + def get_duration(self): + """According to the RFC, either DURATION or DUE should be set + for a task, but never both - implicitly meaning that DURATION + is the difference between DTSTART and DUE (personally I + believe that's stupid. If a task takes five minutes to + complete - say, fill in some simple form that should be + delivered before midnight at new years eve, then it feels + natural for me to define "duration" as five minutes, DTSTART + to "some days before new years eve" and DUE to 20xx-01-01 + 00:00:00 - but I digress. + + This method will return DURATION if set, otherwise the + difference between DUE and DTSTART (if both of them are set). + + Arguably, this logic belongs to the icalendar/vobject layer as + it has nothing to do with the caldav protocol. + + TODO: should be fixed for Event class as well (only difference + is that DTEND is used rather than DUE) and possibly also for + Journal (defaults to one day, probably?) + """ + i = self.icalendar_component + return self._get_duration(i) + + def _get_duration(self, i): + if "DURATION" in i: + return i["DURATION"].dt + elif "DTSTART" in i and self._ENDPARAM in i: + return i[self._ENDPARAM].dt - i["DTSTART"].dt + elif "DTSTART" in i and not isinstance(i[DTSTART], datetime.datetime): + return timedelta(days=1) + else: + return timedelta(0) + ## for backward-compatibility - may be changed to ## icalendar_instance in version 1.0 instance = vobject_instance @@ -2237,6 +2273,7 @@ class Event(CalendarObjectResource): not) """ + _ENDPARAM = "DTEND" pass @@ -2276,6 +2313,8 @@ class Todo(CalendarObjectResource): handle due vs duration. """ + _ENDPARAM = "DUE" + def _next(self, ts=None, i=None, dtstart=None, rrule=None, by=None, no_count=True): """Special logic to fint the next DTSTART of a recurring just-completed task. @@ -2563,41 +2602,10 @@ def uncomplete(self): self.vobject_instance.vtodo.remove(self.vobject_instance.vtodo.completed) self.save() - def get_duration(self): - """According to the RFC, either DURATION or DUE should be set - for a task, but never both - implicitly meaning that DURATION - is the difference between DTSTART and DUE (personally I - believe that's stupid. If a task takes five minutes to - complete - say, fill in some simple form that should be - delivered before midnight at new years eve, then it feels - natural for me to define "duration" as five minutes, DTSTART - to "some days before new years eve" and DUE to 20xx-01-01 - 00:00:00 - but I digress. - - This method will return DURATION if set, otherwise the - difference between DUE and DTSTART (if both of them are set). - - Arguably, this logic belongs to the icalendar/vobject layer as - it has nothing to do with the caldav protocol. - - TODO: should be fixed for Event class as well (only difference - is that DTEND is used rather than DUE) and possibly also for - Journal (defaults to one day, probably?) - """ - i = self.icalendar_component - return self._get_duration(i) - - def _get_duration(self, i): - if "DURATION" in i: - return i["DURATION"].dt - elif "DTSTART" in i and "DUE" in i: - return i["DUE"].dt - i["DTSTART"].dt - else: - return timedelta(0) - + ## TODO: should be moved up to the base class def set_duration(self, duration, movable_attr="DTSTART"): """ - If DTSTART and DUE is already set, one of them should be moved. Which one? I believe that for EVENTS, the DTSTART should remain constant and DTEND should be moved, but for a task, I think the due date may be a hard deadline, hence by default we'll move DTSTART. + If DTSTART and DUE/DTEND is already set, one of them should be moved. Which one? I believe that for EVENTS, the DTSTART should remain constant and DTEND should be moved, but for a task, I think the due date may be a hard deadline, hence by default we'll move DTSTART. TODO: can this be written in a better/shorter way? """ From 4548a532e0a6e6c41d906437d1e59c6a1aacbc8a Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Thu, 5 Jan 2023 19:09:42 +0100 Subject: [PATCH 2/2] More search possibilities - unfortunately not covered by tests yet --- caldav/objects.py | 51 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/caldav/objects.py b/caldav/objects.py index fe180175..a37a614a 100644 --- a/caldav/objects.py +++ b/caldav/objects.py @@ -967,6 +967,8 @@ def search( * event - sets comp_class to event * text attribute search parameters: category, uid, summary, omment, description, location, status + * no-category, no-summary, etc ... search for objects that does not + have those attributes. TODO: WRITE TEST CODE! * expand - do server side expanding of recurring events/tasks * start, end: do a time range search * filters - other kind of filters (in lxml tree format) @@ -1100,8 +1102,6 @@ def build_search_xml_query( ignore_completed2=None, ignore_completed3=None, event=None, - category=None, - class_=None, filters=None, expand=None, start=None, @@ -1198,27 +1198,50 @@ def build_search_xml_query( "unsupported comp class %s for search" % comp_class ) - if category is not None: - filters.append(cdav.PropFilter("CATEGORIES") + cdav.TextMatch(category)) - ## TODO: we probably need to do client side filtering. I would - ## expect --category='e' to fetch anything having the category e, - ## but not including all other categories containing the letter e. - - if class_ is not None: - filters.append(cdav.PropFilter("CLASS") + cdav.TextMatch(class_)) - for other in kwargs: + find_not_defined = other.startswith("no_") + find_defined = other.startswith("has_") + if find_not_defined: + other = other[3:] + if find_defined: + other = other[4:] if other in ( "uid", "summary", "comment", + "class_", + "category", "description", "location", "status", + "due", + "dtstamp", + "dtstart", + "dtend", + "duration", + "priority", ): - filters.append( - cdav.PropFilter(other.upper()) + cdav.TextMatch(kwargs[other]) - ) + ## category and class_ is special + if other.endswith("category"): + ## TODO: we probably need to do client side filtering. I would + ## expect --category='e' to fetch anything having the category e, + ## but not including all other categories containing the letter e. + ## As I read the caldav standard, the latter will be yielded. + target = other.replace("category", "categories") + elif other == "class_": + target = "class" + else: + target = other + + if find_not_defined: + match = cdav.NotDefined() + elif find_defined: + raise NotImplemented( + "Seems not to be supported by the CalDAV protocol? or we can negate? not supported yet, in any case" + ) + else: + match = cdav.TextMatch(kwargs[other]) + filters.append(cdav.PropFilter(target.upper()) + match) else: raise NotImplementedError("searching for %s not supported yet" % other)