Skip to content

Commit

Permalink
[WIP] Add preliminary support for VTODOs
Browse files Browse the repository at this point in the history
Quick PoC that converts VTODOs to VEVENTs before adding them to the
backend, this enables us to not treat tasks as something very special.

If this approach is interesting enough, it should be forbidden that
khal edits tasks as it is out of its scope.

This would fix pimutils#448
  • Loading branch information
evilham committed Mar 17, 2022
1 parent b534cc7 commit 631c4f6
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 9 deletions.
41 changes: 41 additions & 0 deletions khal/icalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,47 @@ def sanitize(vevent, default_timezone, href='', calendar=''):
return vevent


def sanitize_vtodo(vtodo, default_timezone, href='', calendar=''):
"""
cleanup vtodos so they look like vevents for khal
:param vtodo: the vtodo that needs to be cleaned
:type vtodo: icalendar.cal.Todo
:param default_timezone: timezone to apply to start and/or end dates which
were supposed to be localized but which timezone was not understood
by icalendar
:type timezone: pytz.timezone
:param href: used for logging to inform user which .ics files are
problematic
:type href: str
:param calendar: used for logging to inform user which .ics files are
problematic
:type calendar: str
:returns: clean vtodo as vevent
:rtype: icalendar.cal.Event
"""
vdtstart = vtodo.pop('DTSTART', None)
vdue = vtodo.pop('DUE', None)

# it seems to be common for VTODOs to have DUE but no DTSTART
# so we default to that. E.g. NextCloud does something similar
if vdtstart is None and vdue is not None:
vdtstart = vdue

# Based loosely on new_event
event = icalendar.Event()
event.add('dtstart', vdtstart)
event.add('due', vdue)
# Copy common/necessary attributes
for attr in ['uid', 'summary', 'dtend', 'dtstamp', 'description',
'location', 'categories', 'url']:
if attr in vtodo:
event.add(attr, vtodo.pop(attr))

# Chain with event sanitation
return sanitize(event, default_timezone, href=href, calendar=calendar)


def sanitize_timerange(dtstart, dtend, duration=None):
'''return sensible dtstart and end for events that have an invalid or
missing DTEND, assuming the event just lasts one hour.'''
Expand Down
11 changes: 8 additions & 3 deletions khal/khalendar/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from dateutil import parser

from .. import utils
from ..icalendar import assert_only_one_uid, cal_from_ics
from ..icalendar import assert_only_one_uid, cal_from_ics, sanitize_vtodo
from ..icalendar import expand as expand_vevent
from ..icalendar import sanitize as sanitize_vevent
from ..icalendar import sort_key as sort_vevent_key
Expand All @@ -52,6 +52,11 @@

PROTO = 'PROTO'

SANITIZE_MAP = {
'VEVENT': sanitize_vevent,
'VTODO': sanitize_vtodo,
}


class EventType(IntEnum):
DATE = 0
Expand Down Expand Up @@ -226,8 +231,8 @@ def update(self, vevent_str: str, href: str, etag: str='', calendar: str=None) -
"If you want to import it, please use `khal import FILE`."
)
raise NonUniqueUID
vevents = (sanitize_vevent(c, self.locale['default_timezone'], href, calendar) for
c in ical.walk() if c.name == 'VEVENT')
vevents = (SANITIZE_MAP[c.name](c, self.locale['default_timezone'], href, calendar) for
c in ical.walk() if c.name in SANITIZE_MAP.keys())
# Need to delete the whole event in case we are updating a
# recurring event with an event which is either not recurring any
# more or has EXDATEs, as those would be left in the recursion
Expand Down
40 changes: 36 additions & 4 deletions khal/khalendar/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def fromVEvents(cls, events_list, ref=None, **kwargs):
@classmethod
def fromString(cls, event_str, ref=None, **kwargs):
calendar_collection = cal_from_ics(event_str)
events = [item for item in calendar_collection.walk() if item.name == 'VEVENT']
events = [item for item in calendar_collection.walk() if item.name in ['VEVENT', 'VTODO']]
return cls.fromVEvents(events, ref, **kwargs)

def __lt__(self, other):
Expand Down Expand Up @@ -277,7 +277,8 @@ def symbol_strings(self):
'range': '\N{Left right arrow}',
'range_end': '\N{Rightwards arrow to bar}',
'range_start': '\N{Rightwards arrow from bar}',
'right_arrow': '\N{Rightwards arrow}'
'right_arrow': '\N{Rightwards arrow}',
'task': '\N{Pencil}',
}
else:
return {
Expand All @@ -286,7 +287,8 @@ def symbol_strings(self):
'range': '<->',
'range_end': '->|',
'range_start': '|->',
'right_arrow': '->'
'right_arrow': '->',
'task': '(T)',
}

@property
Expand All @@ -304,6 +306,24 @@ def start(self):
"""this should return the start date(time) as saved in the event"""
return self._start

@property
def task(self):
"""this should return whether or not we are representing a task"""
return self._vevents[self.ref].name == 'VTODO'

@property
def task_status(self):
"""nice representation of a task status"""
vstatus = self._vevents[self.ref].get('STATUS', 'NEEDS-ACTION')
status = ' '
if vstatus == 'COMPLETED':
status = 'X'
elif vstatus == 'IN-PROGRESS':
status = '/'
elif vstatus == 'CANCELLED':
status = '-'
return status

@property
def end(self):
"""this should return the end date(time) as saved in the event or
Expand Down Expand Up @@ -427,7 +447,10 @@ def summary(self):
name=name, number=number, suffix=suffix, desc=description, leap=leap,
)
else:
return self._vevents[self.ref].get('SUMMARY', '')
summary = self._vevents[self.ref].get('SUMMARY', '')
if self.task:
summary = '[{state}] {summary}'.format(state=self.task_status, summary=summary)
return summary

def update_summary(self, summary):
self._vevents[self.ref]['SUMMARY'] = summary
Expand Down Expand Up @@ -516,6 +539,14 @@ def _alarm_str(self):
alarmstr = ''
return alarmstr

@property
def _task_str(self):
if self.task:
taskstr = ' ' + self.symbol_strings['task']
else:
taskstr = ''
return taskstr

def format(self, format_string, relative_to, env=None, colors=True):
"""
:param colors: determines if colors codes should be printed or not
Expand Down Expand Up @@ -642,6 +673,7 @@ def format(self, format_string, relative_to, env=None, colors=True):
attributes["repeat-symbol"] = self._recur_str
attributes["repeat-pattern"] = self.recurpattern
attributes["alarm-symbol"] = self._alarm_str
attributes["task-symbol"] = self._task_str
attributes["title"] = self.summary
attributes["organizer"] = self.organizer.strip()
attributes["description"] = self.description.strip()
Expand Down
4 changes: 2 additions & 2 deletions khal/settings/khal.spec
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ bold_for_light_color = boolean(default=True)
# ignored in `ikhal`, where events will always be shown in the color of the
# calendar they belong to.
# The syntax is the same as for :option:`--format`.
agenda_event_format = string(default='{calendar-color}{cancelled}{start-end-time-style} {title}{repeat-symbol}{alarm-symbol}{description-separator}{description}{reset}')
agenda_event_format = string(default='{calendar-color}{cancelled}{start-end-time-style} {title}{repeat-symbol}{alarm-symbol}{task-symbol}{description-separator}{description}{reset}')

# Specifies how each *day header* is formatted.
agenda_day_format = string(default='{bold}{name}, {date-long}{reset}')
Expand All @@ -288,7 +288,7 @@ monthdisplay = monthdisplay(default='firstday')
# but :command:`list` and :command:`calendar`. It is therefore probably a
# sensible choice to include the start- and end-date.
# The syntax is the same as for :option:`--format`.
event_format = string(default='{calendar-color}{cancelled}{start}-{end} {title}{repeat-symbol}{alarm-symbol}{description-separator}{description}{reset}')
event_format = string(default='{calendar-color}{cancelled}{start}-{end} {title}{repeat-symbol}{alarm-symbol}{task-symbol}{description-separator}{description}{reset}')

# When highlight_event_days is enabled, this section specifies how
# the highlighting/coloring of days is handled.
Expand Down

0 comments on commit 631c4f6

Please sign in to comment.