diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e54148826..5ea3a1586 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,13 @@ not released yet * NEW Add default alarms configuration option * FIX defaults for `default_event_duration` and `default_dayevent_duration` where mixed up, `default_dayevent_duration` is the default for all-day events +* NEW event format option `status-symbol` which represents the status of an + event with a symbol (e.g. `✓` for confirmed, `✗` for cancelled, `?` for + tentative) +* NEW event format option `partstat-symbol` which represents the participation + status of an event with a symbol (e.g. `✓` for accepted, `✗` for declined, + `?` for tentative); partication status is shown for the email addresses + configured for the event's calendar 0.11.2 ====== diff --git a/doc/source/usage.rst b/doc/source/usage.rst index dfac789bc..c07ddf390 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -150,6 +150,12 @@ Several options are common to almost all of :program:`khal`'s commands The status of the event (if this event has one), something like `CONFIRMED` or `CANCELLED`. + status-symbol + The status of the event as a symbol, `✓` or `✗` or `?`. + + partstat-symbol + The participation status of the event as a symbol, `✓` or `✗` or `?`. + cancelled The string `CANCELLED` (plus one blank) if the event's status is cancelled, otherwise nothing. diff --git a/khal/cli.py b/khal/cli.py index ec6cbac94..baf8c0090 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -175,6 +175,7 @@ def build_collection(conf, selection): 'color': cal['color'], 'priority': cal['priority'], 'ctype': cal['type'], + 'addresses': cal['addresses'], } collection = khalendar.CalendarCollection( calendars=props, diff --git a/khal/custom_types.py b/khal/custom_types.py index 97000076e..328ed363c 100644 --- a/khal/custom_types.py +++ b/khal/custom_types.py @@ -12,6 +12,7 @@ class CalendarConfiguration(TypedDict): color: str priority: int ctype: str + addresses: str class LocaleConfiguration(TypedDict): diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index e1258e278..9f7339521 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -68,7 +68,8 @@ def __init__(self, color: Optional[str] = None, start: Optional[dt.datetime] = None, end: Optional[dt.datetime] = None, - ) -> None: + addresses: Optional[List[str]] =None, + ): """ :param start: start datetime of this event instance :param end: end datetime of this event instance @@ -87,6 +88,7 @@ def __init__(self, self.color = color self._start: dt.datetime self._end: dt.datetime + self.addresses = addresses if addresses else [] if start is None: self._start = self._vevents[self.ref]['DTSTART'].dt @@ -286,7 +288,12 @@ def symbol_strings(self) -> Dict[str, str]: '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}', + 'cancelled': '\N{Cross mark}', + 'confirmed': '\N{Heavy check mark}', + 'tentative': '?', + 'declined': '\N{Cross mark}', + 'accepted': '\N{Heavy check mark}', } else: return { @@ -295,7 +302,12 @@ def symbol_strings(self) -> Dict[str, str]: 'range': '<->', 'range_end': '->|', 'range_start': '|->', - 'right_arrow': '->' + 'right_arrow': '->', + 'cancelled': 'X', + 'confirmed': 'V', + 'tentative': '?', + 'declined': 'X', + 'accepted': 'V', } @property @@ -554,6 +566,31 @@ def _alarm_str(self) -> str: alarmstr = '' return alarmstr + @property + def _status_str(self) -> str: + if self.status == 'CANCELLED': + statusstr = self.symbol_strings['cancelled'] + elif self.status == 'TENTATIVE': + statusstr = self.symbol_strings['tentative'] + elif self.status == 'CONFIRMED': + statusstr = self.symbol_strings['confirmed'] + else: + statusstr = '' + return statusstr + + @property + def _partstat_str(self) -> str: + partstat = self.partstat + if partstat == 'ACCEPTED': + partstatstr = self.symbol_strings['accepted'] + elif partstat == 'TENTATIVE': + partstatstr = self.symbol_strings['tentative'] + elif partstat == 'DECLINED': + partstatstr = self.symbol_strings['declined'] + else: + partstatstr = '' + return partstatstr + def attributes( self, relative_to: Union[Tuple[dt.date, dt.date], dt.date], @@ -684,6 +721,8 @@ def attributes( attributes["repeat-symbol"] = self._recur_str attributes["repeat-pattern"] = self.recurpattern attributes["alarm-symbol"] = self._alarm_str + attributes["status-symbol"] = self._status_str + attributes["partstat-symbol"] = self._partstat_str attributes["title"] = self.summary attributes["organizer"] = self.organizer.strip() attributes["description"] = self.description.strip() @@ -766,6 +805,14 @@ def delete_instance(self, instance: dt.datetime) -> None: def status(self) -> str: return self._vevents[self.ref].get('STATUS', '') + @property + def partstat(self) -> Optional[str]: + for attendee in self._vevents[self.ref].get('ATTENDEE', []): + for address in self.addresses: + if attendee == 'mailto:' + address: + return attendee.params.get('PARTSTAT', '') + return None + class DatetimeEvent(Event): pass diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py index fc22f28b0..21c85ae8d 100644 --- a/khal/khalendar/khalendar.py +++ b/khal/khalendar/khalendar.py @@ -284,6 +284,7 @@ def _construct_event(self, ref=ref, color=self._calendars[calendar]['color'], readonly=self._calendars[calendar]['readonly'], + addresses=self._calendars[calendar]['addresses'], ) return event diff --git a/khal/settings/khal.spec b/khal/settings/khal.spec index dc9ae1a2c..676d51032 100644 --- a/khal/settings/khal.spec +++ b/khal/settings/khal.spec @@ -71,6 +71,11 @@ readonly = boolean(default=False) # *calendars* subsection will be used. type = option('calendar', 'birthdays', 'discover', default='calendar') +# All email addresses associated with this account, separated by commas. +# For now it is only used to check what participation status ("PARTSTAT") +# belongs to the user. +addresses = force_list(default='') + [sqlite] # khal stores its internal caching database here, by default this will be in the *$XDG_DATA_HOME/khal/khal.db* (this will most likely be *~/.local/share/khal/khal.db*). path = expand_db_path(default=None) diff --git a/tests/configs/small.conf b/tests/configs/small.conf index 9981df2fe..37271e909 100644 --- a/tests/configs/small.conf +++ b/tests/configs/small.conf @@ -8,3 +8,4 @@ [[work]] path = ~/.calendars/work/ readonly = True + addresses = user@example.com diff --git a/tests/conftest.py b/tests/conftest.py index 10bc52bbe..07b5f1128 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,7 @@ def coll_vdirs(tmpdir) -> CollVdirType: color='dark blue', priority=10, ctype='calendar', + addresses='user@example.com', ) vdirs[name] = Vdir(path, '.ics') coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN) @@ -71,7 +72,8 @@ def coll_vdirs_birthday(tmpdir): os.makedirs(path, mode=0o770) readonly = True if name == 'a_calendar' else False calendars[name] = {'name': name, 'path': path, 'color': 'dark blue', - 'readonly': readonly, 'unicode_symbols': True, 'ctype': 'birthdays'} + 'readonly': readonly, 'unicode_symbols': True, 'ctype': 'birthdays', + 'addresses': 'user@example.com'} vdirs[name] = Vdir(path, '.vcf') coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN) coll.default_calendar_name = cal1 diff --git a/tests/event_test.py b/tests/event_test.py index 6dad949ca..d20b90e5f 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -326,6 +326,15 @@ def test_event_rd(): assert event.recurring is True +def test_status_confirmed(): + event = Event.fromString(_get_text('event_dt_status_confirmed'), **EVENT_KWARGS) + assert event.status == 'CONFIRMED' + FORMAT_CALENDAR = ('{calendar-color}{status-symbol}{start-end-time-style} ({calendar}) ' + '{title} [{location}]{repeat-symbol}') + + assert human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) == \ + '✔09:30-10:30 (foobar) An Event []\x1b[0m' + def test_event_d_long(): event_d_long = _get_text('event_d_long') event = Event.fromString(event_d_long, **EVENT_KWARGS) @@ -686,3 +695,37 @@ def test_parameters_description(): assert event.description == ( 'Hey, \n\nJust setting aside some dedicated time to talk about redacted.' ) + +def test_partstat(): + FORMAT_CALENDAR = ( + '{calendar-color}{partstat-symbol}{status-symbol}{start-end-time-style} ({calendar}) ' + '{title} [{location}]{repeat-symbol}' + ) + + event = Event.fromString( + _get_text('event_dt_partstat'), addresses=['jdoe@example.com'], **EVENT_KWARGS) + assert event.partstat == 'ACCEPTED' + assert human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) == \ + '✔09:30-10:30 (foobar) An Event []\x1b[0m' + + event = Event.fromString( + _get_text('event_dt_partstat'), addresses=['another@example.com'], **EVENT_KWARGS) + assert event.partstat == 'DECLINED' + assert human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) == \ + '❌09:30-10:30 (foobar) An Event []\x1b[0m' + + event = Event.fromString( + _get_text('event_dt_partstat'), addresses=['jqpublic@example.com'], **EVENT_KWARGS) + assert event.partstat == 'ACCEPTED' + assert human_formatter(FORMAT_CALENDAR)(event.attributes(dt.date(2014, 4, 9))) == \ + '✔09:30-10:30 (foobar) An Event []\x1b[0m' + +@pytest.mark.xfail +def test_partstat_deligated(): + event = Event.fromString( + _get_text('event_dt_partstat'), addresses=['hcabot@example.com'], **EVENT_KWARGS) + assert event.partstat == 'ACCEPTED' + + event = Event.fromString( + _get_text('event_dt_partstat'), addresses=['iamboss@example.com'], **EVENT_KWARGS) + assert event.partstat == 'ACCEPTED' diff --git a/tests/ics/event_dt_partstat.ics b/tests/ics/event_dt_partstat.ics new file mode 100644 index 000000000..617f1c063 --- /dev/null +++ b/tests/ics/event_dt_partstat.ics @@ -0,0 +1,22 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN +BEGIN:VEVENT +SUMMARY:An Event +DTSTART;TZID=Europe/Berlin:20140409T093000 +DTEND;TZID=Europe/Berlin:20140409T103000 +DTSTAMP:20140401T234817Z +UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;DELEGATED-FROM= + "mailto:iamboss@example.com";CN=Henry Cabot:mailto:hcabot@ + example.com +ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO= + "mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss + @example.com +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Jane Doe + :mailto:jdoe@example.com +ATTENDEE;PARTSTAT=ACCEPTED:mailto:jqpublic@example.com +ATTENDEE;PARTSTAT=DECLINED:mailto:another@example.com +ATTENDEE;PARTSTAT=TENTATIVE:mailto:tent@example.com +END:VEVENT +END:VCALENDAR diff --git a/tests/ics/event_dt_status_confirmed.ics b/tests/ics/event_dt_status_confirmed.ics new file mode 100644 index 000000000..0fddc316a --- /dev/null +++ b/tests/ics/event_dt_status_confirmed.ics @@ -0,0 +1,12 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN +BEGIN:VEVENT +SUMMARY:An Event +DTSTART;TZID=Europe/Berlin:20140409T093000 +DTEND;TZID=Europe/Berlin:20140409T103000 +DTSTAMP:20140401T234817Z +UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR diff --git a/tests/settings_test.py b/tests/settings_test.py index 290f1aeff..a1d6799c7 100644 --- a/tests/settings_test.py +++ b/tests/settings_test.py @@ -40,10 +40,14 @@ def test_simple_config(self): ) comp_config = { 'calendars': { - 'home': {'path': os.path.expanduser('~/.calendars/home/'), - 'readonly': False, 'color': None, 'priority': 10, 'type': 'calendar'}, - 'work': {'path': os.path.expanduser('~/.calendars/work/'), - 'readonly': False, 'color': None, 'priority': 10, 'type': 'calendar'}, + 'home': { + 'path': os.path.expanduser('~/.calendars/home/'), 'readonly': False, + 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], + }, + 'work': { + 'path': os.path.expanduser('~/.calendars/work/'), 'readonly': False, + 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], + }, }, 'sqlite': {'path': os.path.expanduser('~/.local/share/khal/khal.db')}, 'locale': LOCALE_BERLIN, @@ -81,10 +85,10 @@ def test_small(self): 'calendars': { 'home': {'path': os.path.expanduser('~/.calendars/home/'), 'color': 'dark green', 'readonly': False, 'priority': 20, - 'type': 'calendar'}, + 'type': 'calendar', 'addresses': ['']}, 'work': {'path': os.path.expanduser('~/.calendars/work/'), 'readonly': True, 'color': None, 'priority': 10, - 'type': 'calendar'}}, + 'type': 'calendar', 'addresses': ['user@example.com']}}, 'sqlite': {'path': os.path.expanduser('~/.local/share/khal/khal.db')}, 'locale': { 'local_timezone': get_localzone(),