diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 92efba631..7ab0a2b84 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,10 @@ not released yet * 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 address + configured for the event's calendar 0.11.2 ====== diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 08259e296..50512359b 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -153,6 +153,9 @@ Several options are common to almost all of :program:`khal`'s commands 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..b3443e33b 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'], + 'address': cal['address'], } collection = khalendar.CalendarCollection( calendars=props, diff --git a/khal/custom_types.py b/khal/custom_types.py index 97000076e..cda30cecf 100644 --- a/khal/custom_types.py +++ b/khal/custom_types.py @@ -12,6 +12,7 @@ class CalendarConfiguration(TypedDict): color: str priority: int ctype: str + address: str class LocaleConfiguration(TypedDict): diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index 6358f56fe..d3f38e2c9 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: + address: str = '', + ): """ :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.address = address if start is None: self._start = self._vevents[self.ref]['DTSTART'].dt @@ -290,6 +292,8 @@ def symbol_strings(self) -> Dict[str, str]: 'cancelled': '\N{Cross mark}', 'confirmed': '\N{Heavy check mark}', 'tentative': '\N{White question mark ornament}', + 'declined': '\N{Cross mark}', + 'accepted': '\N{Heavy check mark}', } else: return { @@ -302,6 +306,8 @@ def symbol_strings(self) -> Dict[str, str]: 'cancelled': 'X', 'confirmed': 'V', 'tentative': '?', + 'declined': 'X', + 'accepted': 'V', } @property @@ -563,15 +569,28 @@ def _alarm_str(self) -> str: @property def _status_str(self) -> str: if self.status == 'CANCELLED': - statusstr = ' ' + self.symbol_strings['cancelled'] + statusstr = self.symbol_strings['cancelled'] elif self.status == 'TENTATIVE': - statusstr = ' ' + self.symbol_strings['tentative'] + statusstr = self.symbol_strings['tentative'] elif self.status == 'CONFIRMED': - statusstr = ' ' + self.symbol_strings['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, env=None, colors: bool=True): """ :param colors: determines if colors codes should be printed or not @@ -698,6 +717,7 @@ def attributes(self, relative_to, env=None, colors: bool=True): 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() @@ -780,6 +800,13 @@ 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', []): + print(attendee) + if attendee == 'mailto:' + self.address: + return attendee.params.get('PARTSTAT', '') + class DatetimeEvent(Event): pass diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py index fc22f28b0..4b9820b8b 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'], + address=self._calendars[calendar]['address'], ) return event diff --git a/tests/conftest.py b/tests/conftest.py index 10bc52bbe..cc23f721d 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', + address='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', + 'address': '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 b7d7de2e3..19a78887e 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -687,3 +687,38 @@ def test_timezone_creation_with_arbitrary_dates(freeze_ts, event_time): assert len(vtimezone) > 14 assert 'BEGIN:STANDARD' in vtimezone assert 'BEGIN:DAYLIGHT' in vtimezone + + +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'), address='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'), address='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'), address='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'), address='hcabot@example.com', **EVENT_KWARGS) + assert event.partstat == 'ACCEPTED' + + event = Event.fromString( + _get_text('event_dt_partstat'), address='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 125c24b7b..f30f0eee6 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', 'address': ''}, - 'work': {'path': os.path.expanduser('~/.calendars/work/'), - 'readonly': False, 'color': None, 'priority': 10, 'type': 'calendar', 'address': ''}, + 'home': { + 'path': os.path.expanduser('~/.calendars/home/'), 'readonly': False, + 'color': None, 'priority': 10, 'type': 'calendar', 'address': '', + }, + 'work': { + 'path': os.path.expanduser('~/.calendars/work/'), 'readonly': False, + 'color': None, 'priority': 10, 'type': 'calendar', 'address': '', + }, }, 'sqlite': {'path': os.path.expanduser('~/.local/share/khal/khal.db')}, 'locale': LOCALE_BERLIN,