From 7ce0fb958f840cfe98c6e77d4b380b5799ce754d Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 18 Mar 2016 20:29:03 +0100 Subject: [PATCH] Split recurring events properly Previously we moved each VEVENT into its own Item, now we group by UID, if one exists. --- CHANGELOG.rst | 6 +++++ tests/storage/__init__.py | 50 ++++++++++++++++++++++++++++++++++++- vdirsyncer/utils/vobject.py | 27 ++++++++++++++------ 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a321b2e..84d7569 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,12 @@ Package maintainers and users who have to manually update their installation may want to subscribe to `GitHub's tag feed `_. +Version 0.9.3 +============= + +- :storage:`singlefile` and :storage:`http` now handle recurring events + properly. + Version 0.9.2 ============= diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py index dee38b4..8c3a6e6 100644 --- a/tests/storage/__init__.py +++ b/tests/storage/__init__.py @@ -2,6 +2,8 @@ import random +import textwrap + from hypothesis import given import hypothesis.strategies as st @@ -12,7 +14,7 @@ from vdirsyncer.storage.base import Item, normalize_meta_value from vdirsyncer.utils.compat import iteritems, text_type, urlquote, urlunquote from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \ - assert_item_equals, printable_characters_strategy + assert_item_equals, normalize_item, printable_characters_strategy def get_server_mixin(server_name): @@ -285,3 +287,49 @@ class StorageTests(object): # ownCloud replaces "" with "unnamed" s.set_meta('displayname', value) assert s.get_meta('displayname') == normalize_meta_value(value) + + def test_recurring_events(self, s, item_type): + if item_type != 'VEVENT': + pytest.skip('This storage instance doesn\'t support iCalendar.') + + uid = u'abc123' + item = Item(textwrap.dedent(u''' + BEGIN:VCALENDAR + VERSION:2.0 + BEGIN:VEVENT + DTSTART;TZID=Australia/Sydney:20140325T084000 + DTEND;TZID=Australia/Sydney:20140325T101000 + DTSTAMP:20140327T060506Z + UID:{uid} + RECURRENCE-ID;TZID=Australia/Sydney:20140325T083000 + CREATED:20131216T033331Z + DESCRIPTION: + LAST-MODIFIED:20140327T060215Z + LOCATION: + SEQUENCE:1 + STATUS:CONFIRMED + SUMMARY:test Event + TRANSP:OPAQUE + END:VEVENT + BEGIN:VEVENT + DTSTART;TZID=Australia/Sydney:20140128T083000 + DTEND;TZID=Australia/Sydney:20140128T100000 + RRULE:FREQ=WEEKLY;UNTIL=20141208T213000Z;BYDAY=TU + DTSTAMP:20140327T060506Z + UID:{uid} + CREATED:20131216T033331Z + DESCRIPTION: + LAST-MODIFIED:20140222T101012Z + LOCATION: + SEQUENCE:0 + STATUS:CONFIRMED + SUMMARY:Test event + TRANSP:OPAQUE + END:VEVENT + END:VCALENDAR + '''.format(uid=uid)).strip()) + + href, etag = s.upload(item) + + item2, etag2 = s.get(href) + assert normalize_item(item) == normalize_item(item2) diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index a7feabb..2e156fe 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -4,7 +4,7 @@ import hashlib from itertools import chain, tee from . import cached_property, uniq -from .compat import text_type +from .compat import itervalues, text_type def _process_properties(*s): @@ -114,17 +114,24 @@ def hash_item(text): def split_collection(text): assert isinstance(text, text_type) inline = [] - items = [] + items = {} # uid => item + ungrouped_items = [] def inner(item, main): if item.name == u'VTIMEZONE': inline.append(item) elif item.name == u'VCARD': - items.append(item) + ungrouped_items.append(item) elif item.name in (u'VTODO', u'VEVENT', u'VJOURNAL'): - items.append(_Component(main.name, - main.props[:], - [item])) + uid = item.get(u'UID', u'') + wrapper = _Component(main.name, main.props[:], []) + + if uid.strip(): + wrapper = items.setdefault(uid, wrapper) + else: + ungrouped_items.append(wrapper) + + wrapper.subcomponents.append(item) elif item.name in (u'VCALENDAR', u'VADDRESSBOOK'): for subitem in item.subcomponents: inner(subitem, item) @@ -135,7 +142,7 @@ def split_collection(text): for main in _Component.parse(text, multiple=True): inner(main, main) - for item in items: + for item in chain(itervalues(items), ungrouped_items): item.subcomponents.extend(inline) yield u'\r\n'.join(item.dump_lines()) @@ -316,3 +323,9 @@ class _Component(object): break return rv + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default