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

[WIP] Add preliminary support for VTODOs #1118

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
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
10 changes: 8 additions & 2 deletions khal/khalendar/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ..icalendar import assert_only_one_uid, cal_from_ics
from ..icalendar import expand as expand_vevent
from ..icalendar import sanitize as sanitize_vevent
from ..icalendar import sanitize_vtodo
from ..icalendar import sort_key as sort_vevent_key
from .exceptions import (CouldNotCreateDbDir, NonUniqueUID,
OutdatedDbVersionError, UpdateFailed)
Expand All @@ -52,6 +53,11 @@

PROTO = 'PROTO'

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


class EventType(IntEnum):
DATE = 0
Expand Down Expand Up @@ -226,8 +232,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
43 changes: 38 additions & 5 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 = f'[{self.task_status}] {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 Expand Up @@ -730,7 +762,8 @@ class LocalizedEvent(DatetimeEvent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
starttz = getattr(self._vevents[self.ref]['DTSTART'].dt, 'tzinfo', None)
sattr = 'DUE' if self.task else 'DTSTART'
starttz = getattr(self._vevents[self.ref][sattr].dt, 'tzinfo', None)
except KeyError:
msg = (
f"Cannot understand event {kwargs.get('href')} from "
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