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

Added autocomplete of mail addresses to attendees in event editor in interactive mode. #1324

Open
wants to merge 19 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
1 change: 1 addition & 0 deletions AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ Matthew Rademaker - matthew.rademaker [at] gmail [dot] com
Valentin Iovene - val [at] too [dot] gy
Julian Wollrath
Mattori Birnbaum - me [at] mattori [dot] com - https://mattori.com
Piotr Wojciech Dabrowski - piotr.dabrowski [at] htw-berlin [dot] de
1 change: 1 addition & 0 deletions khal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def build_collection(conf, selection):
'priority': cal['priority'],
'ctype': cal['type'],
'addresses': cal['addresses'],
'address_adapter': cal['address_adapter']
}
collection = khalendar.CalendarCollection(
calendars=props,
Expand Down
43 changes: 34 additions & 9 deletions khal/khalendar/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import datetime as dt
import logging
import os
import re
from typing import Dict, List, Optional, Tuple, Type, Union

import icalendar
Expand All @@ -43,6 +44,29 @@
logger = logging.getLogger('khal')


class Attendee:
def __init__(self, defline):
m = re.match(r"(?P<name>.*)\<(?P<mail>.*)\>", defline)
if m is not None and m.group("name") is not None and m.group("mail") is not None:
self.cn = m.group("name").strip()
self.mail = m.group("mail").strip().lower()
else:
self.cn = None
self.mail = defline.strip().lower()

@staticmethod
def format_vcard(vcard):
data = str(vcard).split(":")
if len(data) > 1:
mail = data[1]
else:
mail = str(vcard)
cn = mail
if "CN" in vcard.params:
cn = vcard.params["CN"]
return f"{cn} <{mail}>"


class Event:
"""base Event class for representing a *recurring instance* of an Event

Expand Down Expand Up @@ -493,28 +517,29 @@ def update_location(self, location: str) -> None:
def attendees(self) -> str:
addresses = self._vevents[self.ref].get('ATTENDEE', [])
if not isinstance(addresses, list):
addresses = [addresses, ]
return ", ".join([address.split(':')[-1]
for address in addresses])
return addresses
return ", ".join([Attendee.format_vcard(address) for address in addresses])

def update_attendees(self, attendees: List[str]):
assert isinstance(attendees, list)
attendees = [a.strip().lower() for a in attendees if a != ""]
if len(attendees) > 0:
attendees_o : List[Attendee] = [Attendee(a) for a in attendees if a != ""]
if len(attendees_o) > 0:
# first check for overlaps in existing attendees.
# Existing vCalAddress objects will be copied, non-existing
# vCalAddress objects will be created and appended.
old_attendees = self._vevents[self.ref].get('ATTENDEE', [])
unchanged_attendees = []
vCalAddresses = []
for attendee in attendees:
for attendee in attendees_o:
for old_attendee in old_attendees:
old_email = old_attendee.lstrip("MAILTO:").lower()
if attendee == old_email:
if attendee.mail == old_email:
vCalAddresses.append(old_attendee)
unchanged_attendees.append(attendee)
for attendee in [a for a in attendees if a not in unchanged_attendees]:
item = icalendar.prop.vCalAddress(f'MAILTO:{attendee}')
for attendee in [a for a in attendees_o if a not in unchanged_attendees]:
item = icalendar.prop.vCalAddress(f'MAILTO:{attendee.mail}')
if attendee.cn is not None:
item.params['CN'] = attendee.cn
item.params['ROLE'] = icalendar.prop.vText('REQ-PARTICIPANT')
item.params['PARTSTAT'] = icalendar.prop.vText('NEEDS-ACTION')
item.params['CUTYPE'] = icalendar.prop.vText('INDIVIDUAL')
Expand Down
35 changes: 35 additions & 0 deletions khal/khalendar/khalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
import logging
import os
import os.path
import re
import subprocess
from subprocess import CalledProcessError
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union # noqa

from ..custom_types import CalendarConfiguration, EventCreationTypes, LocaleConfiguration
Expand Down Expand Up @@ -98,15 +101,19 @@ def __init__(self,

self.hmethod = hmethod
self.default_color = default_color
self.default_contacts : List[str] = []
self.multiple = multiple
self.multiple_on_overflow = multiple_on_overflow
self.color = color
self.priority = priority
self.highlight_event_days = highlight_event_days
self._locale = locale
self._contacts: Dict[str, List[str]] = {} # List of mail addresses of contacts
self._backend = backend.SQLiteDb(self.names, dbpath, self._locale)
self._last_ctags: Dict[str, str] = {}
self.update_db()
for cname in self._calendars.keys():
self._contacts_update(cname)

@property
def writable_names(self) -> List[str]:
Expand Down Expand Up @@ -361,12 +368,40 @@ def _needs_update(self, calendar: str, remember: bool=False) -> bool:
self._last_ctags[calendar] = local_ctag
return local_ctag != self._backend.get_ctag(calendar)

def _contacts_update(self, calendar: str) -> None:
if self._calendars[calendar].get('address_adapter') is None:
self._contacts[calendar] = []
adaptercommand = str(self._calendars[calendar].get('address_adapter'))
if adaptercommand == "default":
self._contacts[calendar] = self.default_contacts
else:
self._contacts[calendar] = CalendarCollection._get_contacts(adaptercommand)

@staticmethod
def _get_contacts(adaptercommand: str) -> List[str]:
# Check for both None and "None" because ConfigObj likes to stringify
# configuration
if adaptercommand is None or adaptercommand == "None":
return []
try:
res = subprocess.check_output(["bash", "-c", adaptercommand])
maildata = [re.split(r"\s{2,}", x) for x in res.decode("utf-8").split("\n")]
mails = [f"{x[0]} <{x[2]}>" for x in maildata if len(x) > 1]
return mails
except CalledProcessError:
return []

def update_default_contacts(self, command: str) -> None:
self.default_contacts.clear()
self.default_contacts += CalendarCollection._get_contacts(command)

def _db_update(self, calendar: str) -> None:
"""implements the actual db update on a per calendar base"""
local_ctag = self._local_ctag(calendar)
db_hrefs = {href for href, etag in self._backend.list(calendar)}
storage_hrefs: Set[str] = set()
bdays = self._calendars[calendar].get('ctype') == 'birthdays'
self._contacts_update(calendar)

with self._backend.at_once():
for href, etag in self._storages[calendar].list():
Expand Down
28 changes: 28 additions & 0 deletions khal/settings/khal.spec
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ type = option('calendar', 'birthdays', 'discover', default='calendar')
# belongs to the user.
addresses = force_list(default='')

# The address adapter is a command that will be run using "bash -c" to get
# a list of addresses for autocompletion in the attendee field of a new event
# in interactive mode. This expects the same output as "khard email | tail -n +2"
# since it has only been developed with khard in mind - but other providers
# should be configurable to create the same output, or the parser could
# be extended (see _contacts_update in CalenderCollection in khalendar.py).
# The following options are possible:
#
# * None: No autocompletion for this calendar
# * "default": Use the autocompletion addresses from address_adapter in the
# [default] configuration section (default)
# * "<some command>": Use the output of <some command> as contacts for
# autocompletion
address_adapter = string(default="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 Expand Up @@ -231,6 +246,19 @@ default_dayevent_alarm = timedelta(default='')
# 'ikhal' only)
enable_mouse = boolean(default=True)

# The address adapter is a command that will be run using "bash -c" to get
# a list of addresses for autocompletion in the attendee field of a new event
# in interactive mode. The address_adapter defined in this section will be used
# for auto-completion of attendees in all calendars that have their
# address_adapter set to "default".
# The following options are possible:
#
# * None: No autocompletion
# * "<some command>": Use the output of <some command> as contacts for
# autocompletion. Default:
# "khard email | tail -n +2"
address_adapter = string(default="khard email | tail -n +2")


# The view section contains configuration options that effect the visual appearance
# when using khal and ikhal.
Expand Down
Loading