Repository: pimutils/khal
Pull request #965 (2020-10-06 21:21:25)

#946 Add `--url` option for `khal new` and `{url}` for format in command line
This is a fist attempt to add `URL=` in a VEVENT with khal. you can add it only in command line with `--url` option with `khal new`.

You can display URL with `{url}` in format (both in khal and ikhal).

## Todo
- [x] Add option for khal
- [x] Add an `{url}` element in format
- [x] Add possibility to edit (or add) url with `khal edit`
- [x] ~~Verify URL before save event, throw an error if not correct~~
- [x] Integrate an URL widget in ikhal

ephase: I wonder if URL verification is a good idea because URL verification is tricky, with a big false positive risk.
ephase: Rebase today and decide to not implement URL verification.
ephase: @d7415 No problem, I'm really busy too here and don't have time to participate as I want to... For test, you talk about write some pytest? Or test for user input in khal / ikhal?
d7415: I've added some tests (and a tweak so that setting the URL to `""` removes the field altogether) and pushed to feature/946 in this repo - GitHub doesn't want to let me push to yours. If you're happy with them and can merge them into your branch to update the PR I think we're good to go.
d7415: I'm guessing @ephase is still busy. If I don't hear anything by the end of the week I'll just merge feature/946 from pimutils/khal.

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a465025..623b646 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -24,6 +24,7 @@ not released
    `blank_line_before_day` option
 * FIX `new` keybinding in search no longer crash `ikhal`
 * NEW Improved sorting of events. Sort by `DTSTART`, `DTEND` then `SUMMARY`.
+* NEW Add url input and `{url}` template option
 
 0.10.2
 ======
diff --git a/doc/source/usage.rst b/doc/source/usage.rst
index 52c8ada..af77cfb 100644
--- a/doc/source/usage.rst
+++ b/doc/source/usage.rst
@@ -162,6 +162,9 @@ Several options are common to almost all of :program:`khal`'s commands
        returns
        name@mail.com
 
+   url
+       The URL embedded in the event, otherwise nothing. 
+
    By default, all-day events have no times. To see a start and end time anyway simply
    add `-full` to the end of any template with start/end, for instance
    `start-time` becomes `start-time-full` and will always show start and end times (instead
@@ -409,6 +412,8 @@ Options
 
 * **-u, --until=UNTIL** specify until when a recurring event should run
 
+* **--url** specify the URL element of the event
+
 * **--alarms DURATION,...** will add alarm times as DELTAs comma separated for this event,
   *DURATION* should look like `1day 10minutes` or `1d3H10m`, negative
   *DURATIONs* will set alarm after the start of the event.
diff --git a/khal/cli.py b/khal/cli.py
index a49e68d..e56f793 100644
--- a/khal/cli.py
+++ b/khal/cli.py
@@ -345,10 +345,12 @@ def _get_cli():
                   help=('The format to print the event.'))
     @click.option('--alarms', '-m',
                   help=('Alarm times for the new event as DELTAs comma separated'))
+    @click.option('--url', help=("URI for the event."))
     @click.argument('info', metavar='[START [END | DELTA] [TIMEZONE] [SUMMARY] [:: DESCRIPTION]]',
                     nargs=-1)
     @click.pass_context
-    def new(ctx, calendar, info, location, categories, repeat, until, alarms, format, interactive):
+    def new(ctx, calendar, info, location, categories, repeat, until, alarms, url, format,
+            interactive):
         '''Create a new event from arguments.
 
         START and END can be either dates, times or datetimes, please have a
@@ -394,6 +396,7 @@ def _get_cli():
                 env={"calendars": ctx.obj['conf']['calendars']},
                 until=until,
                 alarms=alarms,
+                url=url,
                 format=format,
             )
         except FatalError as error:
diff --git a/khal/controllers.py b/khal/controllers.py
index e4dbc47..56cd8ba 100644
--- a/khal/controllers.py
+++ b/khal/controllers.py
@@ -280,7 +280,7 @@ def khal_list(collection, daterange=None, conf=None, agenda_format=None,
 
 def new_interactive(collection, calendar_name, conf, info, location=None,
                     categories=None, repeat=None, until=None, alarms=None,
-                    format=None, env=None):
+                    format=None, env=None, url=None):
     try:
         info = parse_datetime.eventinfofstr(
             info, conf['locale'],
@@ -333,7 +333,7 @@ def new_interactive(collection, calendar_name, conf, info, location=None,
     event = new_from_args(
         collection, calendar_name, conf, format=format, env=env,
         location=location, categories=categories,
-        repeat=repeat, until=until, alarms=alarms,
+        repeat=repeat, until=until, alarms=alarms, url=url,
         **info)
 
     echo("event saved")
@@ -344,7 +344,7 @@ def new_interactive(collection, calendar_name, conf, info, location=None,
 
 def new_from_string(collection, calendar_name, conf, info, location=None,
                     categories=None, repeat=None, until=None, alarms=None,
-                    format=None, env=None):
+                    url=None, format=None, env=None):
     """construct a new event from a string and add it"""
     info = parse_datetime.eventinfofstr(
         info, conf['locale'],
@@ -355,14 +355,14 @@ def new_from_string(collection, calendar_name, conf, info, location=None,
     new_from_args(
         collection, calendar_name, conf, format=format, env=env,
         location=location, categories=categories, repeat=repeat,
-        until=until, alarms=alarms, **info
+        until=until, alarms=alarms, url=url, **info
     )
 
 
 def new_from_args(collection, calendar_name, conf, dtstart=None, dtend=None,
                   summary=None, description=None, allday=None, location=None,
                   categories=None, repeat=None, until=None, alarms=None,
-                  timezone=None, format=None, env=None):
+                  timezone=None, url=None, format=None, env=None):
     """Create a new event from arguments and add to vdirs"""
     if isinstance(categories, str):
         categories = list([category.strip() for category in categories.split(',')])
@@ -370,7 +370,7 @@ def new_from_args(collection, calendar_name, conf, dtstart=None, dtend=None,
         event = new_vevent(
             locale=conf['locale'], location=location, categories=categories,
             repeat=repeat, until=until, alarms=alarms, dtstart=dtstart,
-            dtend=dtend, summary=summary, description=description, timezone=timezone,
+            dtend=dtend, summary=summary, description=description, timezone=timezone, url=url,
         )
     except ValueError as error:
         raise FatalError(error)
@@ -428,6 +428,7 @@ def edit_event(event, collection, locale, allow_quit=False, width=80):
     options["categories"] = {"short": "c", "attr": "categories", "none": True}
     options["alarm"] = {"short": "a"}
     options["Delete"] = {"short": "D"}
+    options["url"] = {"short": "u", "attr": "url", "none": True}
 
     now = dt.datetime.now()
 
diff --git a/khal/icalendar.py b/khal/icalendar.py
index 735f4ce..121c07e 100644
--- a/khal/icalendar.py
+++ b/khal/icalendar.py
@@ -71,7 +71,7 @@ def split_ics(ics, random_uid=False, default_timezone=None):
 
 def new_event(locale, dtstart=None, dtend=None, summary=None, timezone=None,
               allday=False, description=None, location=None, categories=None,
-              repeat=None, until=None, alarms=None):
+              repeat=None, until=None, alarms=None, url=None):
     """create a new event
 
     :param dtstart: starttime of that event
@@ -87,6 +87,8 @@ def new_event(locale, dtstart=None, dtend=None, summary=None, timezone=None,
     :param allday: if set to True, we will not transform dtstart and dtend to
         datetime
     :type allday: bool
+    :param url: url of the event
+    :type url: string
     :returns: event
     :rtype: icalendar.Event
     """
@@ -116,6 +118,8 @@ def new_event(locale, dtstart=None, dtend=None, summary=None, timezone=None,
         event.add('location', location)
     if categories:
         event.add('categories', categories)
+    if url:
+        event.add('url', icalendar.vUri(url))
     if repeat and repeat != "none":
         rrule = rrulefstr(repeat, until, locale)
         event.add('rrule', rrule)
diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py
index 8c5f6ff..79991da 100644
--- a/khal/khalendar/event.py
+++ b/khal/khalendar/event.py
@@ -322,6 +322,18 @@ class Event(object):
         else:
             return email
 
+    @property
+    def url(self):
+        if 'URL' not in self._vevents[self.ref]:
+            return ''
+        return self._vevents[self.ref]['URL']
+
+    def update_url(self, url):
+        if url:
+            self._vevents[self.ref]['URL'] = url
+        else:
+            self._vevents[self.ref].pop('URL')
+
     @staticmethod
     def _create_calendar():
         """
@@ -618,6 +630,7 @@ class Event(object):
         attributes["all-day"] = allday
         attributes["categories"] = self.categories
         attributes['uid'] = self.uid
+        attributes['url'] = self.url
 
         if "calendars" in env and self.calendar in env["calendars"]:
             cal = env["calendars"][self.calendar]
diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py
index ddf987e..b08a17d 100644
--- a/khal/ui/__init__.py
+++ b/khal/ui/__init__.py
@@ -960,6 +960,9 @@ class EventDisplay(urwid.WidgetWrap):
         if event.categories != '':
             lines.append(urwid.Text('Categories: ' + event.categories))
 
+        if event.url != '':
+            lines.append(urwid.Text('URL: ' + event.url))
+
         # 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 058226b..c278eb6 100644
--- a/khal/ui/editor.py
+++ b/khal/ui/editor.py
@@ -339,6 +339,7 @@ class EventEditor(urwid.WidgetWrap):
         self.description = event.description
         self.location = event.location
         self.categories = event.categories
+        self.url = event.url
         self.startendeditor = StartEndEditor(
             event.start_local, event.end_local, self._conf,
             self.start_datechange, self.end_datechange,
@@ -376,6 +377,9 @@ class EventEditor(urwid.WidgetWrap):
         self.categories = urwid.AttrMap(ExtendedEdit(
             caption=('', 'Categories:  '), edit_text=self.categories), 'edit'
         )
+        self.url = urwid.AttrMap(ExtendedEdit(
+            caption=('', 'URL:         '), edit_text=self.url), 'edit'
+        )
         self.alarms = AlarmsEditor(self.event)
         self.pile = NListBox(urwid.SimpleFocusListWalker([
             self.summary,
@@ -384,6 +388,7 @@ class EventEditor(urwid.WidgetWrap):
             self.location,
             self.categories,
             self.description,
+            self.url,
             divider,
             self.startendeditor,
             self.recurrenceeditor,


Test patch:
diff --git a/tests/event_test.py b/tests/event_test.py
index 943fff3..3de88ca 100644
--- a/tests/event_test.py
+++ b/tests/event_test.py
@@ -56,6 +56,17 @@ def test_update_simple():
     assert normalize_component(event.raw) == normalize_component(event_updated.raw)
 
 
+def test_add_url():
+    event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS)
+    event.update_url('https://github.com/pimutils/khal')
+    assert 'URL:https://github.com/pimutils/khal' in event.raw
+
+
+def test_get_url():
+    event = Event.fromString(_get_text('event_dt_url'), **EVENT_KWARGS)
+    assert event.url == "https://github.com/pimutils/khal"
+
+
 def test_no_end():
     """reading an event with neither DTEND nor DURATION"""
     event = Event.fromString(_get_text('event_dt_no_end'), **EVENT_KWARGS)
@@ -77,6 +88,12 @@ def test_do_not_save_empty_description():
     assert 'DESCRIPTION' not in event.raw
 
 
+def test_do_not_save_empty_url():
+    event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS)
+    event.update_url('')
+    assert 'URL' not in event.raw
+
+
 def test_remove_existing_location_if_set_to_empty():
     event = Event.fromString(_get_text('event_dt_simple_updated'), **EVENT_KWARGS)
     event.update_location('')
@@ -89,6 +106,12 @@ def test_remove_existing_description_if_set_to_empty():
     assert 'DESCRIPTION' not in event.raw
 
 
+def test_remove_existing_url_if_set_to_empty():
+    event = Event.fromString(_get_text('event_dt_url'), **EVENT_KWARGS)
+    event.update_url('')
+    assert 'URL' not in event.raw
+
+
 def test_update_remove_categories():
     event = Event.fromString(_get_text('event_dt_simple_updated'), **EVENT_KWARGS)
     event_nocat = Event.fromString(_get_text('event_dt_simple_nocat'), **EVENT_KWARGS)
diff --git a/tests/ics/event_dt_url.ics b/tests/ics/event_dt_url.ics
new file mode 100644
index 0000000..b2cf7ca
--- /dev/null
+++ b/tests/ics/event_dt_url.ics
@@ -0,0 +1,12 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN
+BEGIN:VEVENT
+SUMMARY:An Event
+URL:https://github.com/pimutils/khal
+DTSTART;TZID=Europe/Berlin;VALUE=DATE-TIME:20140409T093000
+DTEND;TZID=Europe/Berlin;VALUE=DATE-TIME:20140409T103000
+DTSTAMP;VALUE=DATE-TIME:20140401T234817Z
+UID:V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU
+END:VEVENT
+END:VCALENDAR