summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Geier <geier@lostpackets.de>2023-11-15 21:08:51 +0100
committerGitHub <noreply@github.com>2023-11-15 21:08:51 +0100
commit214886de48bf706f897592cd01a9de6250ba3df4 (patch)
treead4282135d9b7ae30b55dc78672ba7215a036850
parentffef88483f703d7470bd0f2054bfcf6915200ca0 (diff)
parent765bae2f64f1c8b20eb2e3cf176d0cef9f84cd23 (diff)
Merge pull request #1271 from pimutils/feature/status
Better support for status and initial support for partstat
-rw-r--r--CHANGELOG.rst7
-rw-r--r--doc/source/usage.rst6
-rw-r--r--khal/cli.py1
-rw-r--r--khal/custom_types.py1
-rw-r--r--khal/khalendar/event.py53
-rw-r--r--khal/khalendar/khalendar.py1
-rw-r--r--khal/settings/khal.spec5
-rw-r--r--tests/configs/small.conf1
-rw-r--r--tests/conftest.py4
-rw-r--r--tests/event_test.py43
-rw-r--r--tests/ics/event_dt_partstat.ics22
-rw-r--r--tests/ics/event_dt_status_confirmed.ics12
-rw-r--r--tests/settings_test.py16
13 files changed, 162 insertions, 10 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index e5414882..5ea3a158 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 dfac789b..c07ddf39 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 ec6cbac9..baf8c009 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 97000076..328ed363 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 e1258e27..9f733952 100644
--- a/khal/khalendar/event.py
+++ b/khal/khalendar/event.py
@@ -68,7 +68,8 @@ class Event:
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 @@ class Event:
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 @@ class Event:
'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 @@ class Event:
'range': '<->',
'range_end': '->|',
'range_start': '|->',
- 'right_arrow': '->'
+ 'right_arrow': '->',
+ 'cancelled': 'X',
+ 'confirmed': 'V',
+ 'tentative': '?',
+ 'declined': 'X',
+ 'accepted': 'V',
}
@property
@@ -554,6 +566,31 @@ class Event:
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 @@ class Event:
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 @@ class Event:
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 fc22f28b..21c85ae8 100644
--- a/khal/khalendar/khalendar.py
+++ b/khal/khalendar/khalendar.py
@@ -284,6 +284,7 @@ class CalendarCollection:
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 dc9ae1a2..676d5103 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 9981df2f..37271e90 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 10bc52bb..07b5f112 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 6dad949c..d20b90e5 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 00000000..617f1c06
--- /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 00000000..0fddc316
--- /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 290f1aef..a1d6799c 100644
--- a/tests/settings_test.py
+++ b/tests/settings_test.py
@@ -40,10 +40,14 @@ class TestSettings:
)
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 @@ class TestSettings:
'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(),