Skip to content

Commit 6bf9c8e

Browse files
authored
Merge pull request #878 from d7415/vcf_dates
Add support for other VCARD dates (e.g. X-ANNIVERSARY)
2 parents 741f870 + a5a4928 commit 6bf9c8e

File tree

6 files changed

+239
-61
lines changed

6 files changed

+239
-61
lines changed

AUTHORS.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ Tobias Brummer - hallo [at] t0bybr.de - https://t0bybr.de
3636
Amanda Hickman - amanda [at] velociraptor [dot] info
3737
Raef Coles - raefcoles [at] gmail [dot] com
3838
Nito Martinez - nito [at] qindel [dot] com - http://qindel.com http://theqvd.com
39-
Florian Wehner - florian [at] whnr [dot] de
39+
Florian Wehner - florian [at] whnr [dot] de
40+
Martin Stone

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ may want to subscribe to `GitHub's tag feed
88
<https://github.com/geier/khal/tags.atom>`_.
99

1010

11+
0.10.2
12+
======
13+
not released
14+
15+
* NEW Parse `X-ANNIVERSARY`, `ANNIVERSARY` and `X-ABDATE` fields from vcards.
16+
17+
1118
0.10.1
1219
======
1320
2019-03-30

khal/khalendar/backend.py

Lines changed: 107 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -240,58 +240,83 @@ def update(self, vevent_str: str, href: str, etag: str='', calendar: str=None) -
240240
stuple = (vevent_str, etag, href, calendar)
241241
self.sql_ex(sql_s, stuple)
242242

243-
def update_birthday(self, vevent_str: str, href: str, etag: str='', calendar: str=None) -> None:
244-
"""
245-
XXX write docstring
243+
def update_vcf_dates(self, vevent_str: str, href: str, etag: str='',
244+
calendar: str=None) -> None:
245+
"""insert events from a vcard into the db
246+
247+
This is will parse BDAY, ANNIVERSARY, X-ANNIVERSARY and X-ABDATE fields.
248+
It will also look for any X-ABLABEL fields associated with an X-ABDATE
249+
and use that in the event description.
250+
251+
:param vevent_str: contact (vcard) to be parsed.
252+
:param href: href of the card on the server, if this href already
253+
exists in the db the card gets updated. If no href is given, a
254+
random href is chosen and it is implied that this card does not yet
255+
exist on the server, but will be uploaded there on next sync.
256+
:param etag: the etag of the vcard, if this etag does not match the
257+
remote etag on next sync, this card will be updated from the server.
258+
For locally created vcards this should not be set
246259
"""
247260
assert calendar is not None
248261
assert href is not None
249-
self.delete(href, calendar=calendar)
262+
# Delete all event entries for this contact
263+
self.deletelike(href + '%', calendar=calendar)
250264
ical = utils.cal_from_ics(vevent_str)
251265
vcard = ical.walk()[0]
252-
if 'BDAY' in vcard.keys():
253-
bday = vcard['BDAY']
254-
if isinstance(bday, list):
255-
logger.warning(
256-
'Vcard {0} in collection {1} has more than one '
257-
'BIRTHDAY, will be skipped and not be available '
258-
'in khal.'.format(href, calendar)
259-
)
260-
return
261-
try:
262-
if bday[0:2] == '--' and bday[3] != '-':
263-
bday = '1900' + bday[2:]
264-
orig_bday = False
266+
for key in vcard.keys():
267+
if key in ['BDAY', 'X-ANNIVERSARY', 'ANNIVERSARY'] or key.endswith('X-ABDATE'):
268+
date = vcard[key]
269+
if isinstance(date, list):
270+
logger.warning(
271+
'Vcard {0} in collection {1} has more than one '
272+
'{2}, will be skipped and not be available '
273+
'in khal.'.format(href, calendar, key)
274+
)
275+
continue
276+
try:
277+
if date[0:2] == '--' and date[3] != '-':
278+
date = '1900' + date[2:]
279+
orig_date = False
280+
else:
281+
orig_date = True
282+
date = parser.parse(date).date()
283+
except ValueError:
284+
logger.warning(
285+
'cannot parse {0} in {1} in collection {2}'.format(key, href, calendar))
286+
continue
287+
if 'FN' in vcard:
288+
name = vcard['FN']
265289
else:
266-
orig_bday = True
267-
bday = parser.parse(bday).date()
268-
except ValueError:
269-
logger.warning(
270-
'cannot parse BIRTHDAY in {0} in collection {1}'.format(href, calendar))
271-
return
272-
if 'FN' in vcard:
273-
name = vcard['FN']
274-
else:
275-
n = vcard['N'].split(';')
276-
name = ' '.join([n[1], n[2], n[0]])
277-
vevent = icalendar.Event()
278-
vevent.add('dtstart', bday)
279-
vevent.add('dtend', bday + dt.timedelta(days=1))
280-
if bday.month == 2 and bday.day == 29: # leap year
281-
vevent.add('rrule', {'freq': 'YEARLY', 'BYYEARDAY': 60})
282-
else:
283-
vevent.add('rrule', {'freq': 'YEARLY'})
284-
if orig_bday:
285-
vevent.add('x-birthday',
286-
'{:04}{:02}{:02}'.format(bday.year, bday.month, bday.day))
287-
vevent.add('x-fname', name)
288-
vevent.add('summary', '{0}\'s birthday'.format(name))
289-
vevent.add('uid', href)
290-
vevent_str = vevent.to_ical().decode('utf-8')
291-
self._update_impl(vevent, href, calendar)
292-
sql_s = ('INSERT INTO events (item, etag, href, calendar) VALUES (?, ?, ?, ?);')
293-
stuple = (vevent_str, etag, href, calendar)
294-
self.sql_ex(sql_s, stuple)
290+
n = vcard['N'].split(';')
291+
name = ' '.join([n[1], n[2], n[0]])
292+
vevent = icalendar.Event()
293+
vevent.add('dtstart', date)
294+
vevent.add('dtend', date + dt.timedelta(days=1))
295+
if date.month == 2 and date.day == 29: # leap year
296+
vevent.add('rrule', {'freq': 'YEARLY', 'BYYEARDAY': 60})
297+
else:
298+
vevent.add('rrule', {'freq': 'YEARLY'})
299+
description = get_vcard_event_description(vcard, key)
300+
if orig_date:
301+
if key == 'BDAY':
302+
xtag = 'x-birthday'
303+
elif key.endswith('ANNIVERSARY'):
304+
xtag = 'x-anniversary'
305+
else:
306+
xtag = 'x-abdate'
307+
vevent.add('x-ablabel', description)
308+
vevent.add(xtag,
309+
'{:04}{:02}{:02}'.format(date.year, date.month, date.day))
310+
vevent.add('x-fname', name)
311+
vevent.add('summary',
312+
'{0}\'s {1}'.format(name, description))
313+
vevent.add('uid', href + key)
314+
vevent_str = vevent.to_ical().decode('utf-8')
315+
self._update_impl(vevent, href + key, calendar)
316+
sql_s = ('INSERT INTO events (item, etag, href, calendar)'
317+
' VALUES (?, ?, ?, ?);')
318+
stuple = (vevent_str, etag, href + key, calendar)
319+
self.sql_ex(sql_s, stuple)
295320

296321
def _update_impl(self, vevent: icalendar.cal.Event, href: str, calendar: str) -> None:
297322
"""insert `vevent` into the database
@@ -407,6 +432,23 @@ def delete(self, href: str, etag: Any=None, calendar: str=None):
407432
sql_s = 'DELETE FROM events WHERE href = ? AND calendar = ?;'
408433
self.sql_ex(sql_s, (href, calendar))
409434

435+
def deletelike(self, href: str, etag: Any=None, calendar: str=None):
436+
"""
437+
removes events from the db that match an SQL 'like' statement,
438+
439+
:param href: The pattern of hrefs to delete. May contain SQL wildcards
440+
like '%'
441+
:param etag: only there for compatibility with vdirsyncer's Storage,
442+
we always delete
443+
:returns: None
444+
"""
445+
assert calendar is not None
446+
for table in ['recs_loc', 'recs_float']:
447+
sql_s = 'DELETE FROM {0} WHERE href LIKE ? AND calendar = ?;'.format(table)
448+
self.sql_ex(sql_s, (href, calendar))
449+
sql_s = 'DELETE FROM events WHERE href LIKE ? AND calendar = ?;'
450+
self.sql_ex(sql_s, (href, calendar))
451+
410452
def list(self, calendar):
411453
""" list all events in `calendar`
412454
@@ -611,3 +653,22 @@ def calc_shift_deltas(vevent: icalendar.Event) -> Tuple[dt.timedelta, dt.timedel
611653
except KeyError:
612654
duration = vevent['DURATION'].dt
613655
return start_shift, duration
656+
657+
658+
def get_vcard_event_description(vcard: icalendar.cal.Component, key: str) -> str:
659+
if key == 'BDAY':
660+
return 'birthday'
661+
elif key.endswith('ANNIVERSARY'):
662+
return 'anniversary'
663+
elif key.endswith('X-ABDATE'):
664+
desc_key = key[:-8] + 'X-ABLABEL'
665+
if desc_key in vcard.keys():
666+
return vcard[desc_key]
667+
else:
668+
desc_key = key[:-8] + 'X-ABLabel'
669+
if desc_key in vcard.keys():
670+
return vcard[desc_key]
671+
else:
672+
return 'custom event from vcard'
673+
else:
674+
return 'unknown event from vcard'

khal/khalendar/event.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,23 @@ def export_ics(self, path):
351351

352352
@property
353353
def summary(self):
354-
bday = self._vevents[self.ref].get('x-birthday', None)
355-
if bday:
356-
number = self.start_local.year - int(bday[:4])
354+
description = None
355+
date = self._vevents[self.ref].get('x-birthday', None)
356+
if date:
357+
description = 'birthday'
358+
else:
359+
date = self._vevents[self.ref].get('x-anniversary', None)
360+
if date:
361+
description = 'anniversary'
362+
else:
363+
date = self._vevents[self.ref].get('x-abdate', None)
364+
if date:
365+
description = self._vevents[self.ref].get('x-ablabel', 'custom event')
366+
367+
if date:
368+
number = self.start_local.year - int(date[:4])
357369
name = self._vevents[self.ref].get('x-fname', None)
358-
if int(bday[4:6]) == 2 and int(bday[6:8]) == 29:
370+
if int(date[4:6]) == 2 and int(date[6:8]) == 29:
359371
leap = ' (29th of Feb.)'
360372
else:
361373
leap = ''
@@ -367,8 +379,8 @@ def summary(self):
367379
suffix = 'rd'
368380
else:
369381
suffix = 'th'
370-
return '{name}\'s {number}{suffix} birthday{leap}'.format(
371-
name=name, number=number, suffix=suffix, leap=leap,
382+
return '{name}\'s {number}{suffix} {desc}{leap}'.format(
383+
name=name, number=number, suffix=suffix, desc=description, leap=leap,
372384
)
373385
else:
374386
return self._vevents[self.ref].get('SUMMARY', '')

khal/khalendar/khalendar.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ def _db_update(self, calendar: str):
305305
local_ctag = self._local_ctag(calendar)
306306
db_hrefs = set(href for href, etag in self._backend.list(calendar))
307307
storage_hrefs = set()
308+
bdays = self._calendars[calendar].get('ctype') == 'birthdays'
308309

309310
with self._backend.at_once():
310311
for href, etag in self._storages[calendar].list():
@@ -314,7 +315,14 @@ def _db_update(self, calendar: str):
314315
logger.debug('Updating {0} because {1} != {2}'.format(href, etag, db_etag))
315316
self._update_vevent(href, calendar=calendar)
316317
for href in db_hrefs - storage_hrefs:
317-
self._backend.delete(href, calendar=calendar)
318+
if bdays:
319+
for sh in storage_hrefs:
320+
if href.startswith(sh):
321+
break
322+
else:
323+
self._backend.delete(href, calendar=calendar)
324+
else:
325+
self._backend.delete(href, calendar=calendar)
318326
self._backend.set_ctag(local_ctag, calendar=calendar)
319327
self._last_ctags[calendar] = local_ctag
320328

@@ -324,7 +332,7 @@ def _update_vevent(self, href: str, calendar: str) -> bool:
324332
event, etag = self._storages[calendar].get(href)
325333
try:
326334
if self._calendars[calendar].get('ctype') == 'birthdays':
327-
update = self._backend.update_birthday
335+
update = self._backend.update_vcf_dates
328336
else:
329337
update = self._backend.update
330338
update(event.raw, href=href, etag=etag, calendar=calendar)

0 commit comments

Comments
 (0)