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

Recurrent event with an exception instance leads to multiple RECURRENCE-ID values in the exception instance #394

Open
dozed opened this issue Apr 29, 2024 · 4 comments

Comments

@dozed
Copy link
Contributor

dozed commented Apr 29, 2024

I created an iCalendar object as follow:

  • Create bi-weekly event
  • Edit the title of the second instance at 2024-04-25 in Thunderbird

This leads to an iCalendar object with two events, one master and one exception instance:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VEVENT
UID:c26921f4-0653-11ef-b756-58ce2a14e2e5
DTSTART;VALUE=DATE:20240411
DTEND;VALUE=DATE:20240412
DTSTAMP:20240429T181103Z
LAST-MODIFIED:20240429T181103Z
RRULE:FREQ=WEEKLY;INTERVAL=2
SEQUENCE:1
SUMMARY:Test 1
X-MOZ-GENERATION:1
END:VEVENT
BEGIN:VEVENT
UID:c26921f4-0653-11ef-b756-58ce2a14e2e5
RECURRENCE-ID;VALUE=DATE:20240425
DTSTART;VALUE=DATE:20240425
DTEND;VALUE=DATE:20240426
CREATED:20240429T181031Z
DTSTAMP:20240429T181103Z
LAST-MODIFIED:20240429T181103Z
SEQUENCE:1
SUMMARY:Test 1 (edited)
X-MOZ-GENERATION:1
END:VEVENT
END:VCALENDAR

When searching for the event via caldav, two instances are returned. The second instance contains the RECURRENCE-ID field twice as shown in the following test case:

def testRecurringDateWithExceptionSearch(self):
    self.skip_on_compatibility_flag("read_only")
    self.skip_on_compatibility_flag("no_recurring")
    c = self._fixCalendar()

    # evr2 is a bi-weekly event starting 2024-04-11
    e = c.save_event(evr2)

    r = c.search(
        start=datetime(2024, 3, 31, 0, 0),
        end=datetime(2024, 5, 4, 0, 0, 0),
        event=True,
        expand=True,
    )

    assert len(r) == 2

    assert 'RRULE' not in r[0].data
    assert 'RRULE' not in r[1].data

    assert isinstance(r[0].icalendar_component['RECURRENCE-ID'], icalendar.vDDDTypes)
    # OK:
    # vDDDTypes(2024-04-11, Parameters({'VALUE': 'DATE'}))

    assert len(r[1].icalendar_component['RECURRENCE-ID']) == 1
    # fails, since there are multiple values:
    # [vDDDTypes(2024-04-25, Parameters({'VALUE': 'DATE'})),
    #  vDDDTypes(2024-04-25, Parameters({'VALUE': 'DATE'}))]

A possible solution would be to add a check for an already existing RECURRENCE-ID value here:

caldav/caldav/objects.py

Lines 2005 to 2007 in f4d5cd2

for occurrence in recurrings:
occurrence.add("RECURRENCE-ID", occurrence.get("DTSTART"))
calendar.add_component(occurrence)

Another option would be to just overwrite the value.

@dozed dozed changed the title Recurrent event with an exception instance leads two multiple RECURRENCE-ID values in the exception instance Recurrent event with an exception instance leads multiple RECURRENCE-ID values in the exception instance Apr 29, 2024
@dozed dozed changed the title Recurrent event with an exception instance leads multiple RECURRENCE-ID values in the exception instance Recurrent event with an exception instance leads to multiple RECURRENCE-ID values in the exception instance Apr 29, 2024
@dozed
Copy link
Contributor Author

dozed commented Apr 29, 2024

Here is a minimal working example test case:

import socket
import tempfile
import threading
import time
from datetime import datetime

import icalendar
import pytest
import radicale
import radicale.server
import requests
from caldav import DAVClient
from caldav.objects import Calendar


@pytest.fixture
def radicale_calendar():
    radicale_host = 'localhost'
    radicale_port = 52121
    url = f'http://{radicale_host}:{radicale_port}/'
    username = 'user'
    password = 'some-password'

    serverdir = tempfile.TemporaryDirectory()
    serverdir.__enter__()

    configuration = radicale.config.load()
    configuration.update(
        {
            'server': {'hosts': f'127.0.0.1:{radicale_port}'},
            'storage': {'filesystem_folder': serverdir.name}
        }
    )

    shutdown_socket, shutdown_socket_out = socket.socketpair()
    radicale_thread = threading.Thread(
        target=radicale.server.serve,
        args=(configuration, shutdown_socket_out),
    )
    radicale_thread.start()

    i = 0
    while True:
        try:
            requests.get(url)
            break
        except:
            time.sleep(0.05)
            i += 1
            assert i < 100

    client = DAVClient(url=url, username=username, password=password)
    principal = client.principal()
    calendar = principal.make_calendar(
        name='Yep', cal_id='123'
    )

    yield calendar

    shutdown_socket.close()
    serverdir.__exit__(None, None, None)


recurring_event_with_exception = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VEVENT
UID:c26921f4-0653-11ef-b756-58ce2a14e2e5
DTSTART;VALUE=DATE:20240411
DTEND;VALUE=DATE:20240412
DTSTAMP:20240429T181103Z
LAST-MODIFIED:20240429T181103Z
RRULE:FREQ=WEEKLY;INTERVAL=2
SEQUENCE:1
SUMMARY:Test 1
X-MOZ-GENERATION:1
END:VEVENT
BEGIN:VEVENT
UID:c26921f4-0653-11ef-b756-58ce2a14e2e5
RECURRENCE-ID;VALUE=DATE:20240425
DTSTART;VALUE=DATE:20240425
DTEND;VALUE=DATE:20240426
CREATED:20240429T181031Z
DTSTAMP:20240429T181103Z
LAST-MODIFIED:20240429T181103Z
SEQUENCE:1
SUMMARY:Test (edited)
X-MOZ-GENERATION:1
END:VEVENT
END:VCALENDAR"""


def test_expanding_caldav_search_with_recurrent_event_having_exception(radicale_calendar: Calendar):
    radicale_calendar.save_event(recurring_event_with_exception)

    r = radicale_calendar.search(
        start=datetime(2024, 3, 31, 0, 0),
        end=datetime(2024, 5, 4, 0, 0, 0),
        event=True,
        expand=True,
    )

    assert len(r) == 2

    assert 'RRULE' not in r[0].data
    assert 'RRULE' not in r[1].data

    assert isinstance(r[0].icalendar_component['RECURRENCE-ID'], icalendar.vDDDTypes)
    # vDDDTypes(2024-04-11, Parameters({'VALUE': 'DATE'}))

    # Fails:
    assert isinstance(r[1].icalendar_component['RECURRENCE-ID'], icalendar.vDDDTypes)
    # [vDDDTypes(2024-04-25, Parameters({'VALUE': 'DATE'})),
    #  vDDDTypes(2024-04-25, Parameters({'VALUE': 'DATE'}))]

@tobixen
Copy link
Member

tobixen commented Apr 30, 2024

Thanks for your report, you've done a good job debugging and pinpointing the issue as well as writing up test code. Would you care to make it into a pull request? :-)

@dozed
Copy link
Contributor Author

dozed commented Apr 30, 2024

Sure, I created a draft pull request: #395

There is still an issue with Xandikos on my machine. Do you have an idea maybe what this could be?

@tobixen
Copy link
Member

tobixen commented Apr 30, 2024

I've had different problems with different versions of Xandikos. Do you get an error message?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants