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

Delete recurring event? #35

Open
embie27 opened this issue Dec 17, 2018 · 13 comments
Open

Delete recurring event? #35

embie27 opened this issue Dec 17, 2018 · 13 comments

Comments

@embie27
Copy link

embie27 commented Dec 17, 2018

How to delete only a single occurrence of a recurring event? The delete()-method deletes every occurrence.

@tobixen
Copy link
Member

tobixen commented Mar 8, 2019

Sorry the slow response.

Deleting a single instance of a recurring event is slightly nontrivial, see https://blog.jonudell.net/2008/08/28/specifying-exceptions-to-recurring-calendar-events/amp/ for details.

It is within the scope of the caldav library to support this somehow, but I will not have capacity to look into it anytime soon.

@tobixen
Copy link
Member

tobixen commented Feb 5, 2024

It's probably several years still until I get time to deal with this.

The problem is as such: when running a .delete() on a recurrence instance (that is defined as a calendar event or todo having a recurrence-id set), then a DELETE-command is sent to the server on the URL. The problem is that the URL for the recurrence instance is the same as the URL for the recurring instance (and all other recurrence instance), hence everything will be deleted, and that's probably not what one wanted.

But what does one actually want to do when running DELETE on a recurrence instance?

  • Does one want to cancel the instance? Then it's probably best and easiest to switch STATUS to CANCELLED.
  • However, the proper thing to do is probably tu use the EXRULE property. "delete" and "cancel" are two slightly different things, "delete" means that he recurrence instance was never going to happen in the first place, cancel means that it was planned but got cancelled.
  • A recurring instance is an exception to the general rule. Maybe a DELETE actually means that the exception should be deleted?
  • Since the client library can't guess what the caller actually wants, perhaps the right thing to do is to throw an error?

Probably it's needed to specify with a separate optional parameter how to deal with recurrence instances - probably with the default being "raise an error".

And yes, this is a bit related to #379, because maybe it's needed to load the full recurrence set and put it back to the server to get things done correctly on all servers.

@julien4215
Copy link

I don't really know the subject but when I looked the references I saw that EXRULE is deprecated (https://icalendar.org/iCalendar-RFC-5545/a-3-deprecated-features.html). I guess that can be useful for the issue.

@ptrba
Copy link

ptrba commented May 22, 2024

Commenting on:

But what does one actually want to do when running DELETE on a recurrence instance?

I have a recurrent series of events but attendees are assigned only on specific instances of the series. The use case is, that I want to remove the assignment of the attendee(s). I came across the issue because I wanted to delete the event in this case. However, this is not needed, I can simply remove the attendee(s) an leave the event without attendees in the calendar.

@ptrba
Copy link

ptrba commented May 22, 2024

There seems to be a deeper issue here. On a related discussion from sabre dav https://groups.google.com/g/sabredav-discuss/c/M82DQRJTr4A?pli=1 they suggest to work on the level of 'calendar' rather than 'event' objects. Rather than fetching single events, icalendar.Calendar object can be fetched and modified.

This solves the problem of deleting single events from a recurrent series of events. Simply get the calendar object which contains the recurrent series and all possible modifications of single events. Then add/remove/update the corresponding items, then put the calendar object back.

Important, there are 2 concepts of calendars.

  1. The calendar used by python-caldav as caldav.DAVClient(...).principal().calendar(). I call it parent here. This is not the calendar we are referring to.
  2. There is an icalendar.Calendar object which is a container for multiple subcomponents. It is basically what you get if you fetch an .ics from a url. This is what we mean by calendar object in what follows.

python-caldav does not seem to support working with calendar objects directly, but it is easy to work around this. Here is a working example. Create a series where just one item has an attendee. In this example we first create the 2 corresponding events wrapped by a calendar object. Notice their identical uid. Tested with SOGo. The will be located at <base_url>/6B-664A5280-F-5B831280.ics.

data = """
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:recurrence with attendee one single item
DTSTART;TZID=Europe/Zurich:20240101T090000
DTEND;TZID=Europe/Zurich:20240101T180000
UID:6B-664A5280-F-5B831280
DESCRIPTION:this is the recurrent series
TRANSP:OPAQUE
RRULE:FREQ=WEEKLY;BYDAY=TU,WE,TH
END:VEVENT
BEGIN:VEVENT
SUMMARY:single item
DTSTART;TZID=Europe/Zurich:20240605T090000
DTEND;TZID=Europe/Zurich:20240605T170000
UID:6B-664A5280-F-5B831280
DESCRIPTION:this is the single item assigning a attendee to just one event
ATTENDEE:foo.bar@corge.baz
RECURRENCE-ID:20240605T070000Z
END:VEVENT
END:VCALENDAR
"""

parent = caldav.DAVClient(...).principal().calendar() 

ical_calendar = icalendar.Calendar.from_ical(data)

events = [event for event in ical_calendar.subcomponents if isinstance(event,icalendar.Event)]

assert len(events) == 2
assert events[0].get('RECURRENCE-ID') is None
assert events[1].get('RECURRENCE-ID').dt == datetime.datetime(2024,6,5,7,0,tzinfo=datetime.timezone.utc)

caldav.CalendarObjectResource(
    client=parent.client,
    data=ical_calendar.to_ical().decode('utf-8'),
    parent=parent
).save()

# Important, do not provide a comp_class here. If we put comp_class='Event' we will only receive
# one of the original 2 events
object_by_id = calendar_.calendar.object_by_uid('6B-664A5280-F-5B831280',comp_class=None)

# object_by_id is an event rather than a Calendar,
# but no problem, the desired calendar is present in the underlying data
ical_calendar_ = icalendar.Calendar.from_ical(object_by_id.data)

events = [event for event in ical_calendar_.subcomponents if isinstance(event,icalendar.Event)]
assert len(events) == 2
assert events[0].get('RECURRENCE-ID') is None
assert events[1].get('RECURRENCE-ID').dt == datetime.datetime(2024, 6, 5, 7, 0, tzinfo=datetime.timezone.utc)

ical_calendar__ = icalendar.Calendar()
ical_calendar__.add_component(events[0])

caldav.CalendarObjectResource(
    client=parent.client,
    data=ical_calendar__.to_ical().decode('utf-8'),
    parent=parent
).save()

object_by_id = calendar_.calendar.object_by_uid('6B-664A5280-F-5B831280', comp_class=None)
ical_calendar_ = icalendar.Calendar.from_ical(object_by_id.data)

events = [event for event in ical_calendar_.subcomponents if isinstance(event, icalendar.Event)]
assert len(events) == 1

python-caldav would definitely benefit from a better support for icalendar.calendar objects, but this is another issue.

@tobixen
Copy link
Member

tobixen commented May 23, 2024

A "calendar" is a collection of events/tasks/journals. In the CalDAV specifications, it's defined to have a CalDAV URL and it supports operations like search, hence the caldav.Calendar class mirrors the CalDAV specification. In the icalendar definition it's possible to bundle together independent events/tasks/journals in a VCALENDAR-object, however the CalDAV protocol will return each event/task/journal as separate VCALENDAR-objects. The exception is with recurring tasks, there a VCALENDAR-object containing the base definition and the instances are returned.

What is probably poorly documented is that a caldav.Event-object in some cases may be a recurrent event with all the recurrence instance data included (and sometimes with the recurrence instance data autogenerated, either at the server side or client side), other times such an object may contain only a recurrence instance, and yet in other cases it may contain only the definition of the recurring event.

The Calendar.search-method has two relevant options, expand will autogenerate instances, and split_expanded will split each of those into separate Event-objects rather than including all the recurrence-instances in one Event-object. The latter is set to true by default.

# Important, do not provide a comp_class here. If we put comp_class='Event' we will only receive
# one of the original 2 events
object_by_id = calendar_.calendar.object_by_uid('6B-664A5280-F-5B831280',comp_class=None)

This sounds like a bug. I should look into it when I'm more awake.

ical_calendar_ = icalendar.Calendar.from_ical(object_by_id.data)

This should be equivalent to:

ical_calendar_ = object_by_id.icalendar_instance

I'm recommending to do this in the documentation:

ical_calendar_ = object_by_id.icalendar_component

Which will give the icalendar component object (event/task/journal) rather than the icalendar calendar object. and hence throw away all the recurrence-data fra object_by_id if there exist any. The documentation is probably a bit weak at this point.

python-caldav would definitely benefit from a better support for icalendar.calendar objects, but this is another issue

I disagree on that one - but the possibility to edit overridden recurrence instances (and handling them at all) is probably not very well thought-through in the python-caldav library.

@ptrba
Copy link

ptrba commented May 23, 2024

Great explanation. So it all boils down to the following:

VCALENDAR objects can be used to fully control recurring events with multiple single instances. Add, modify or delete all or some of the individual instances.

I can confirm the issue with object_by_id for accessing the VCALENDAR object:

# Important, do not provide a comp_class here. If we put comp_class='Event' we will only receive
# one of the original 2 events
object_by_id = calendar_.calendar.object_by_uid('6B-664A5280-F-5B831280',comp_class=caldav.Event)

# object_by_id is an event rather than a Calendar,
# but no problem, the desired calendar is present in the underlying data
ical_calendar_ = object_by_id.icalendar_instance

events = [event for event in ical_calendar_.subcomponents if isinstance(event,icalendar.Event)]
assert len(events) == 1 # expecting 2

@tobixen
Copy link
Member

tobixen commented May 23, 2024

Great explanation. So it all boils down to the following:

VCALENDAR objects can be used to fully control recurring events with multiple single instances. Add, modify or delete all or some of the individual instances.

Yes. By fetching it through object_by_id, or using calendar.search(..., split_expanded=False) and then accessing the icalendar data either through the raw event.data, event.vobject_instance or (recommended) event.icalendar_instance, followed by an event.save() , it should be possible to make any changes on recurrence instances. If it doesn't, it's a bug.

There also exists higher-level methods for editing participants in the caldav library - though, it's very poorly tested as it was done as part of a project that lost traction at some point.

I can confirm the issue with object_by_id for accessing the VCALENDAR object:

I will fork it out as a separate issue.

@tobixen
Copy link
Member

tobixen commented May 23, 2024

@ptrba - could you please have a look at #398? First of all - I haven't slept much over the last two nights, perhaps the text there make sense only in my own head, I'd like a peer-review on weather the text is readable at all :-) The other thing is weather my suggestions make sense at all or not.

@tobixen
Copy link
Member

tobixen commented May 23, 2024

Also, I think I've concluded that an Event.delete() on an Event containing Ionly) (a) recurrence instance(s) should be transformed into an edit, setting STATUS:CANCELLED. This will solve this issue. The actual work will be done when working on #398.

@tobixen
Copy link
Member

tobixen commented May 23, 2024

Also, @ptrba (sorry for the noise here) - why did you consider removing all participants is a better idea than setting status=cancelled?

@ptrba
Copy link

ptrba commented May 28, 2024

Also, @ptrba (sorry for the noise here) - why did you consider removing all participants is a better idea than setting status=cancelled?

Probably equivalent. Have not tried. I went into the direction of editing the events in Calendar object because I have a lot of changes being performed on the object and I wanted to have full control over the entries. Cancelling events will mean they will be dangling around. May or not be problem for the performance, but definitely creates cognitive overhead. The solution presented here works flawlessly and it is clean.

What bothered me most was the following Problem (obfuscated and simplified, but there is a real use case behind it).

I have a recurrent series of events, say lunch from Monday til Friday from 12-13 pm. I have a special date where I add an attendee, say 1st of May. This will create an clone with the recurrence-id on 20xx0501-1200. Now, we decide to change lunch time from 12 to 12.15. This should affect the regular recurrent series but also the special date (it is only special because it has another attendee). Now I need to change the recurrence-id of the special event. I can do this by either:

  • Cancel the event with the 20xx0501-1200 and create a new one with 20xx0501-1215.
  • Deleting the event with the 20xx0501-1200 and create a new one with 20xx0501-1215.
  • Modify recurrence-id of the event with recurrence-id 20xx0501-1200 in the Calendar object.
    I do not know the exact behaviour of the server if there is a mismatch of the recurrence-id. Prefer not try to take this risk.

wrt to your issue #398: I do not fully understand the role of expansion here. But this will of course depend on the use case. In my use case I use a client side representation which is very close to the original icalendar events.

@tobixen
Copy link
Member

tobixen commented May 28, 2024

Honestly, I have no idea what is best - and what is best may depend quite a bit on he client and server. Actually I believe the CalDAV and icalendar standards are a mess. I started using the CalDAV library because I didn't want to get my hands dirty with low-lever work on those protocols, but had to fix some bugs in the library and soon enough the owner of the project put the maintainer-hat on my head :-) That being said, I believe that according to the standards 12:15 is not a valid time for the RECURRENCE-ID because it does not match up with the DTSTART and RRULE of the original object. I believe the right thing to do is to keep the RECURRENCE-ID as it is (because it is the ID of the "1st of may Lunch", even if the time is changed), but move the DTSTART. Similar, if the lunch for some reason or another should be moved from a Friday to a Saturday, I believe the proper thing is to keep the RECURRENCE-ID pointing to Friday but let DTSTART move to Saturday. (Digression: if I'm not mistaken, in Russia and China if a fixed-date public holiday like the 1st of May happens to be on a Thursday, often the Friday after is declared to be a public holiday, but to compensate for this extra holiday, the next Saturday is defined to be a regular working day! Well, the 1s of May lunch is anyway cancelled, but the 2nd of May lunch is postponed with 8 days!)

As for "expansion", say one searches for "all events between 10:00 and 14:00 on Wednesday in a week", how should the lunch be returned? With expand=False the server will return the icalendar data it has stored (lunch at 12:00 on a Monday three years ago, but with RRULE set to "daily, five times a week", possibly with all the special case recurrence instances included, and it's up to the client to . If expand=True then the server will return a recurrence-instance (automatically generated, if it doesn't already exist) covering the lunch Wednesday in a week.

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

No branches or pull requests

4 participants