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

Better support for status and initial support for partstat #1271

Merged
merged 5 commits into from
Nov 15, 2023
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
======
Expand Down
6 changes: 6 additions & 0 deletions doc/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions khal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions khal/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class CalendarConfiguration(TypedDict):
color: str
priority: int
ctype: str
addresses: str


class LocaleConfiguration(TypedDict):
Expand Down
53 changes: 50 additions & 3 deletions khal/khalendar/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions khal/khalendar/khalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions khal/settings/khal.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tests/configs/small.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
[[work]]
path = ~/.calendars/work/
readonly = True
addresses = [email protected]
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def coll_vdirs(tmpdir) -> CollVdirType:
color='dark blue',
priority=10,
ctype='calendar',
addresses='[email protected]',
)
vdirs[name] = Vdir(path, '.ics')
coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN)
Expand All @@ -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': '[email protected]'}
vdirs[name] = Vdir(path, '.vcf')
coll = CalendarCollection(calendars=calendars, dbpath=':memory:', locale=LOCALE_BERLIN)
coll.default_calendar_name = cal1
Expand Down
43 changes: 43 additions & 0 deletions tests/event_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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=['[email protected]'], **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=['[email protected]'], **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=['[email protected]'], **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=['[email protected]'], **EVENT_KWARGS)
assert event.partstat == 'ACCEPTED'

event = Event.fromString(
_get_text('event_dt_partstat'), addresses=['[email protected]'], **EVENT_KWARGS)
assert event.partstat == 'ACCEPTED'
22 changes: 22 additions & 0 deletions tests/ics/event_dt_partstat.ics
Original file line number Diff line number Diff line change
@@ -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:[email protected]";CN=Henry Cabot:mailto:hcabot@
example.com
ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO=
"mailto:[email protected]";CN=The Big Cheese:mailto:iamboss
@example.com
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Jane Doe
:mailto:[email protected]
ATTENDEE;PARTSTAT=ACCEPTED:mailto:[email protected]
ATTENDEE;PARTSTAT=DECLINED:mailto:[email protected]
ATTENDEE;PARTSTAT=TENTATIVE:mailto:[email protected]
END:VEVENT
END:VCALENDAR
12 changes: 12 additions & 0 deletions tests/ics/event_dt_status_confirmed.ics
Original file line number Diff line number Diff line change
@@ -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
16 changes: 10 additions & 6 deletions tests/settings_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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': ['[email protected]']}},
'sqlite': {'path': os.path.expanduser('~/.local/share/khal/khal.db')},
'locale': {
'local_timezone': get_localzone(),
Expand Down