Repository: pimutils/khal
Pull request #1127 (2022-05-04 22:06:52)

Pr/973


Patch:
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 3270fc0..58c8a8e 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -44,3 +44,4 @@ Yorick Barbanneau - git [at] epha [dot] se - https://xieme-art.org
 Florian Lassenay - pizzacoca [at] aquilenet [dot] fr
 Simon Crespeau - simon [at] art-e-toile [dot] com - https://www.art-e-toile.com
 Fred Thomsen - me [at] fredthomsen [dot] net - http://fredthomsen.net
+Robin Schubert
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 7903d45..165337f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -11,6 +11,8 @@ may want to subscribe to `GitHub's tag feed
 0.10.5
 ======
 not released
+* NEW Add widget to interactive event editor that allows adding attendees as
+  comma separated list of email addresses
 
 * NEW Add support for Python 3.10
 
diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py
index 69874a5..10729ac 100644
--- a/khal/khalendar/event.py
+++ b/khal/khalendar/event.py
@@ -477,6 +477,43 @@ class Event:
         else:
             self._vevents[self.ref].pop('LOCATION')
 
+    @property
+    def attendees(self):
+        addresses = self._vevents[self.ref].get('ATTENDEE', [])
+        if not isinstance(addresses, list):
+            addresses = [addresses, ]
+        return ", ".join([address.split(':')[-1]
+                          for address in addresses])
+
+    def update_attendees(self, attendees):
+        assert isinstance(attendees, list)
+        attendees = [a.strip().lower() for a in attendees if a != ""]
+        if len(attendees) > 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 old_attendee in old_attendees:
+                    old_email = old_attendee.lstrip("MAILTO:").lower()
+                    if attendee == 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}')
+                item.params['ROLE'] = icalendar.prop.vText('REQ-PARTICIPANT')
+                item.params['PARTSTAT'] = icalendar.prop.vText('NEEDS-ACTION')
+                item.params['CUTYPE'] = icalendar.prop.vText('INDIVIDUAL')
+                item.params['RSVP'] = icalendar.prop.vText('TRUE')
+                # TODO use khard here to receive full information from email address
+                vCalAddresses.append(item)
+
+            self._vevents[self.ref]['ATTENDEE'] = vCalAddresses
+        else:
+            self._vevents[self.ref].pop('ATTENDEE')
+
     @property
     def categories(self):
         try:
@@ -649,6 +686,7 @@ class Event:
         if attributes["description"]:
             attributes["description-separator"] = " :: "
         attributes["location"] = self.location.strip()
+        attributes["attendees"] = self.attendees
         attributes["all-day"] = allday
         attributes["categories"] = self.categories
         attributes['uid'] = self.uid
diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py
index 982a8f9..77c8776 100644
--- a/khal/ui/__init__.py
+++ b/khal/ui/__init__.py
@@ -965,6 +965,11 @@ class EventDisplay(urwid.WidgetWrap):
         if event.url != '':
             lines.append(urwid.Text('URL: ' + event.url))
 
+        if event.attendees != '':
+            lines.append(urwid.Text('Attendees:'))
+            for attendee in event.attendees.split(', '):
+                lines.append(urwid.Text(f'  - {attendee}'))
+
         # start and end time/date
         if event.allday:
             startstr = event.start_local.strftime(self._conf['locale']['dateformat'])
diff --git a/khal/ui/editor.py b/khal/ui/editor.py
index 0ed3775..d8e341a 100644
--- a/khal/ui/editor.py
+++ b/khal/ui/editor.py
@@ -338,6 +338,7 @@ class EventEditor(urwid.WidgetWrap):
 
         self.description = event.description
         self.location = event.location
+        self.attendees = event.attendees
         self.categories = event.categories
         self.url = event.url
         self.startendeditor = StartEndEditor(
@@ -365,17 +366,25 @@ class EventEditor(urwid.WidgetWrap):
         )
         self.description = urwid.AttrMap(
             ExtendedEdit(
-                caption=('', 'Description: '),
+                caption=('', 'Description:  '),
                 edit_text=self.description,
                 multiline=True
             ),
             'edit'
         )
         self.location = urwid.AttrMap(ExtendedEdit(
-            caption=('', 'Location:    '), edit_text=self.location), 'edit'
+            caption=('', 'Location:     '), edit_text=self.location), 'edit'
         )
         self.categories = urwid.AttrMap(ExtendedEdit(
-            caption=('', 'Categories:  '), edit_text=self.categories), 'edit'
+            caption=('', 'Categories:   '), edit_text=self.categories), 'edit'
+        )
+        self.attendees = urwid.AttrMap(
+            ExtendedEdit(
+                caption=('', 'Attendees: '),
+                edit_text=self.attendees,
+                multiline=True
+            ),
+            'edit'
         )
         self.url = urwid.AttrMap(ExtendedEdit(
             caption=('', 'URL:         '), edit_text=self.url), 'edit'
@@ -390,6 +399,8 @@ class EventEditor(urwid.WidgetWrap):
             self.description,
             self.url,
             divider,
+            self.attendees,
+            divider,
             self.startendeditor,
             self.recurrenceeditor,
             divider,
@@ -428,6 +439,8 @@ class EventEditor(urwid.WidgetWrap):
             return True
         if get_wrapped_text(self.url) != self.event.url:
             return True
+        if get_wrapped_text(self.attendees) != self.event.attendees:
+            return True
         if self.startendeditor.changed or self.calendar_chooser.changed:
             return True
         if self.recurrenceeditor.changed:
@@ -440,6 +453,7 @@ class EventEditor(urwid.WidgetWrap):
         self.event.update_summary(get_wrapped_text(self.summary))
         self.event.update_description(get_wrapped_text(self.description))
         self.event.update_location(get_wrapped_text(self.location))
+        self.event.update_attendees(get_wrapped_text(self.attendees).split(','))
         self.event.update_categories(get_wrapped_text(self.categories).split(','))
         self.event.update_url(get_wrapped_text(self.url))
 


Test patch:
diff --git a/tests/event_test.py b/tests/event_test.py
index 11781e0..6b52d8a 100644
--- a/tests/event_test.py
+++ b/tests/event_test.py
@@ -3,7 +3,7 @@ import datetime as dt
 import pytest
 import pytz
 from freezegun import freeze_time
-from icalendar import vRecur, vText
+from icalendar import Parameters, vCalAddress, vRecur, vText
 
 from khal.khalendar.event import (AllDayEvent, Event, FloatingEvent,
                                   LocalizedEvent, create_timezone)
@@ -480,6 +480,41 @@ def test_event_alarm():
     assert event.alarms == [(dt.timedelta(-1, 82800), vText('new event'))]
 
 
+def test_event_attendees():
+    event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS)
+    assert event.attendees == ""
+    event.update_attendees(["this-does@not-exist.de", ])
+    assert event.attendees == "this-does@not-exist.de"
+    assert isinstance(event._vevents[event.ref].get('ATTENDEE', []), list)
+    assert isinstance(event._vevents[event.ref].get('ATTENDEE', [])[0], vCalAddress)
+    assert str(event._vevents[event.ref].get('ATTENDEE', [])[0]) == "MAILTO:this-does@not-exist.de"
+
+    event.update_attendees(["this-does@not-exist.de", "also-does@not-exist.de"])
+    assert event.attendees == "this-does@not-exist.de, also-does@not-exist.de"
+
+    assert isinstance(event._vevents[event.ref].get('ATTENDEE', []), list)
+    assert len(event._vevents[event.ref].get('ATTENDEE', [])) == 2
+    assert isinstance(event._vevents[event.ref].get('ATTENDEE', [])[0], vCalAddress)
+
+    # test if parameters from existing vCalAddress objects will be preserved
+    new_address = vCalAddress("MAILTO:mail.address@not-exist.de")
+    new_address.params = Parameters(
+        {'CN': 'Real Name',
+         'PARTSTAT': 'NEEDS-ACTION',
+         'ROLE': 'REQ-PARTICIPANT',
+         'RSVP': 'TRUE'}
+    )
+    event._vevents[event.ref]['ATTENDEE'] = [new_address, ]
+    event.update_attendees(["another.mailaddress@not-exist.de", "mail.address@not-exist.de"])
+    assert event.attendees == "mail.address@not-exist.de, another.mailaddress@not-exist.de"
+    address = [a for a in event._vevents[event.ref].get('ATTENDEE', [])
+               if str(a) == "MAILTO:mail.address@not-exist.de"]
+    assert len(address) == 1
+    address = address[0]
+    assert address.params.get('CN', None) is not None
+    assert address.params['CN'] == "Real Name"
+
+
 def test_create_timezone_static():
     gmt = pytz.timezone('Etc/GMT-8')
     assert create_timezone(gmt).to_ical().split() == [