Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New features for calendar_cli aka plann #244

Merged
merged 2 commits into from
Jan 14, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 78 additions & 47 deletions caldav/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -1618,6 +1641,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
Expand Down Expand Up @@ -2223,6 +2248,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
Expand All @@ -2237,6 +2296,7 @@ class Event(CalendarObjectResource):
not)
"""

_ENDPARAM = "DTEND"
pass


Expand Down Expand Up @@ -2276,6 +2336,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.
Expand Down Expand Up @@ -2563,41 +2625,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?
"""
Expand Down