From a8b0c48a0b57db1bdbbd0f72a0bf0d8fc0cb42e6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 10 Dec 2020 17:14:29 +0100 Subject: [PATCH] Added naive way to expand recurring events. see #62 --- frontend/src/components/Calendar.vue | 6 +- modoboa_radicale/backends/caldav_.py | 86 +++++++++++++++++++++++----- modoboa_radicale/serializers.py | 5 +- 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/Calendar.vue b/frontend/src/components/Calendar.vue index d83285d..5762ae2 100644 --- a/frontend/src/components/Calendar.vue +++ b/frontend/src/components/Calendar.vue @@ -90,7 +90,8 @@ export default { select: this.selectCallback, eventClick: this.eventClickCallback, eventDrop: this.eventDropCallback, - eventResize: this.eventResizeCallback + eventResize: this.eventResizeCallback, + scrollTime: '09:00:00' }) this.calendar.render() this.addEventSources(this.calendars) @@ -128,6 +129,9 @@ export default { this.calendar.addEvent(event, true) }, eventClickCallback (info) { + if (!info.event.startEditable) { + return + } var evtCalendar = info.event.extendedProps.calendar var calType = (evtCalendar.domain) ? 'shared' : 'user' this.$router.push({ diff --git a/modoboa_radicale/backends/caldav_.py b/modoboa_radicale/backends/caldav_.py index c79ea9a..ab203c0 100644 --- a/modoboa_radicale/backends/caldav_.py +++ b/modoboa_radicale/backends/caldav_.py @@ -6,6 +6,7 @@ import caldav from caldav.elements import dav, ical from caldav import Calendar +from dateutil.relativedelta import relativedelta import vobject from django.utils import timezone @@ -21,7 +22,7 @@ class Caldav_Backend(CalendarBackend): def __init__(self, username, password, calendar=None): """Constructor.""" - super(Caldav_Backend, self).__init__(calendar) + super().__init__(calendar) server_url = smart_str( param_tools.get_global_parameter("server_location")) self.client = caldav.DAVClient( @@ -30,40 +31,53 @@ def __init__(self, username, password, calendar=None): if self.calendar: self.remote_cal = Calendar(self.client, calendar.encoded_path) - def _serialize_event(self, event): + def _serialize_event( + self, + vevent, + start=None, + end=None, + all_day: bool = False, + **kwargs) -> dict: """Convert a vevent to a dictionary.""" - vevent = event.vobject_instance.vevent description = ( vevent.description.value if "description" in vevent.contents else "" ) + event_id = vevent.uid.value result = { - "id": vevent.uid.value, + "id": event_id, "title": vevent.summary.value, "color": self.calendar.color, "description": description, "calendar": self.calendar, - "attendees": [] + "attendees": [], + # Quick way to disable edition of recurring events until + # we support it. + "editable": not kwargs.get("recurring", False), + **kwargs } - if isinstance(vevent.dtstart.value, datetime.datetime): - all_day = False + if start is None and end is None: start = vevent.dtstart.value end = vevent.dtend.value + if isinstance(start, datetime.datetime): + all_day = all_day else: tz = timezone.get_current_timezone() all_day = True start = tz.localize( - datetime.datetime.combine( - vevent.dtstart.value, datetime.time.min)) + datetime.datetime.combine(start, datetime.time.min)) end = tz.localize( - datetime.datetime.combine( - vevent.dtend.value, datetime.time.min)) + datetime.datetime.combine(end, datetime.time.min)) result.update({ "allDay": all_day, "start": start, "end": end }) if "attendee" in vevent.contents: + if "organizer" in vevent.contents: + organizer = vevent.organizer.value.replace("mailto:", "") + if organizer != self.calendar.mailbox.full_address: + result["editable"] = False for attendee in vevent.contents["attendee"]: email = ( attendee.value @@ -77,6 +91,51 @@ def _serialize_event(self, event): }) return result + def _serialize_events(self, event, start, end) -> list: + """Convert this event to a list of dictionaries. + + In case of recurring event, we will generate as much + dictionaries as necessary, according to given start and end + dates. + + """ + tz = timezone.get_current_timezone() + result = [] + for vevent in event.vobject_instance.vevent_list: + rruleset = vevent.getrruleset() + if rruleset: + all_day = True + duration = relativedelta( + vevent.dtend.value, vevent.dtstart.value) + if isinstance(vevent.dtstart.value, datetime.datetime): + all_day = False + for date in list(rruleset): + if date.tzinfo is None: + date = tz.localize(date) + if date >= start and date <= end: + result += [ + self._serialize_event( + vevent, date, date + duration, all_day=all_day, + recurring=True + ) + ] + else: + options = {} + if "recurrence-id" in vevent.contents: + # Remove previously expanded event because it has + # been overriden + recurrence_id = vevent.recurrence_id.value + options["recurring"] = True + for (pos, item) in enumerate(result): + if ( + item["id"] == vevent.uid.value and + item["start"] == recurrence_id + ): + del result[pos] + break + result += [self._serialize_event(vevent, **options)] + return result + def create_calendar(self, url): """Create a new calendar.""" self.client.mkcalendar(url) @@ -147,14 +206,15 @@ def get_event(self, uid): """Retrieve and event using its uid.""" url = "{}/{}.ics".format(self.remote_cal.url.geturl(), uid) event = self.remote_cal.event_by_url(url) - return self._serialize_event(event) + vevent = event.vobject_instance.vevent + return self._serialize_event(vevent) def get_events(self, start, end): """Retrieve a list of events.""" orig_events = self.remote_cal.date_search(start, end) events = [] for event in orig_events: - events.append(self._serialize_event(event)) + events += self._serialize_events(event, start, end) return events def delete_event(self, uid): diff --git a/modoboa_radicale/serializers.py b/modoboa_radicale/serializers.py index f756f56..da598a6 100644 --- a/modoboa_radicale/serializers.py +++ b/modoboa_radicale/serializers.py @@ -127,10 +127,13 @@ class EventSerializer(serializers.Serializer): class ROEventSerializer(EventSerializer): """Event serializer for read operations.""" + editable = serializers.BooleanField() + recurring = serializers.BooleanField(default=False) + def __init__(self, *args, **kwargs): """Set calendar field based on type.""" calendar_type = kwargs.pop("calendar_type") - super(ROEventSerializer, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields["calendar"] = ( UserCalendarSerializer() if calendar_type == "user" else SharedCalendarSerializer()