From d79aa41df42348cd1ff2e924f0dbf3a5134832a8 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 5 Jan 2026 15:34:46 +0100 Subject: [PATCH 1/4] WIP: replaced FullCalendar by Vuetify internal calendar --- docker/Dockerfile.radicale.dev | 2 +- frontend/package.json | 2 +- .../src/components/calendars/EventForm.vue | 50 +- frontend/src/views/calendars/CalendarView.vue | 556 ++++++++++++++---- frontend/yarn.lock | 8 +- modoboa/calendars/backends/caldav_.py | 20 +- modoboa/calendars/serializers.py | 23 +- modoboa/calendars/viewsets.py | 4 +- 8 files changed, 497 insertions(+), 168 deletions(-) diff --git a/docker/Dockerfile.radicale.dev b/docker/Dockerfile.radicale.dev index e93b4f9ed..4133af841 100644 --- a/docker/Dockerfile.radicale.dev +++ b/docker/Dockerfile.radicale.dev @@ -4,7 +4,7 @@ LABEL org.opencontainers.image.authors="Antoine Nguyen " WORKDIR /app -RUN pip install Radicale pytz radicale-modoboa-auth-oauth2 +RUN pip install -U Radicale pytz radicale-modoboa-auth-oauth2 EXPOSE 5232 diff --git a/frontend/package.json b/frontend/package.json index 51b6ae5cb..5f8274f7c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,7 +41,7 @@ "vue-router": "^4.6.3", "vue3-apexcharts": "^1.10.0", "vue3-gettext": "^4.0.0-beta.1", - "vuetify": "^3.10.11" + "vuetify": "^3.11.0" }, "devDependencies": { "@vitejs/plugin-basic-ssl": "^1.1.0", diff --git a/frontend/src/components/calendars/EventForm.vue b/frontend/src/components/calendars/EventForm.vue index b38972452..1a0fd6f65 100644 --- a/frontend/src/components/calendars/EventForm.vue +++ b/frontend/src/components/calendars/EventForm.vue @@ -26,7 +26,7 @@ :error-messages="formErrors.start" density="compact" variant="outlined" - type="datetime-local" + :type="dateType" class="mr-4" />
@@ -133,14 +133,27 @@ const title = computed(() => { return props.event ? $gettext('Edit event') : $gettext('New event') }) +const dateType = computed(() => { + return form.value.allDay ? 'date' : 'datetime-local' +}) + watch( () => props.info, (value) => { if (value) { + const start = + typeof value.start === 'number' + ? DateTime.fromMillis(value.start) + : DateTime.fromJSDate(value.start) + const end = + typeof value.end === 'number' + ? DateTime.fromMillis(value.end) + : DateTime.fromJSDate(value.end) + const format = value.timed ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd' form.value = { - start: DateTime.fromISO(value.startStr).toFormat('yyyy-MM-dd HH:mm'), - end: DateTime.fromISO(value.endStr).toFormat('yyyy-MM-dd HH:mm'), - allDay: value.allDay, + start: start.toFormat(format), + end: end.toFormat(format), + allDay: !value.timed, } } }, @@ -153,10 +166,9 @@ watch( if (value) { form.value = { ...value } form.value.calendar = form.value.calendar.pk - form.value.start = DateTime.fromISO(value.start).toFormat( - 'yyyy-MM-dd HH:mm' - ) - form.value.end = DateTime.fromISO(value.end).toFormat('yyyy-MM-dd HH:mm') + const format = form.value.timed ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd' + form.value.start = DateTime.fromMillis(value.start).toFormat(format) + form.value.end = DateTime.fromMillis(value.end).toFormat(format) api.getAttendees().then((resp) => { attendees.value = resp.data }) @@ -176,11 +188,21 @@ async function saveEvent() { } working.value = true try { - var data = JSON.parse(JSON.stringify(form.value)) - const fromDate = new Date(data.start) - data.start = fromDate.toISOString() - const toDate = new Date(data.end) - data.end = toDate.toISOString() + const data = { + title: form.value.title, + description: form.value.description, + allDay: form.value.allDay, + attendees: form.value.attendees, + calendar: form.value.calendar, + } + + if (data.allDay) { + data.start_date = form.value.start + data.end_date = form.value.end + } else { + data.start = new Date(form.value.start).toISOString() + data.end = new Date(form.value.end).toISOString() + } if (!props.event) { await api.createUserEvent(form.value.calendar, data) busStore.displayNotification({ msg: $gettext('Event added') }) diff --git a/frontend/src/views/calendars/CalendarView.vue b/frontend/src/views/calendars/CalendarView.vue index 94f81d5d1..9d1ea2bb1 100644 --- a/frontend/src/views/calendars/CalendarView.vue +++ b/frontend/src/views/calendars/CalendarView.vue @@ -1,43 +1,128 @@ + + diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6b8f1b821..1b09454a2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2449,10 +2449,10 @@ vue@^3.4.31: "@vue/server-renderer" "3.5.24" "@vue/shared" "3.5.24" -vuetify@^3.10.11: - version "3.10.11" - resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.10.11.tgz#d2da1ffe9ffcd15e9192c31e08cc0469e57e705b" - integrity sha512-hfllXT0/C3O5nZyIRalaDU7ClMIrKrKAbjH0T8xbSUb7FcJrHOqPZfEkSXwrKxajv6EA1rwEOvCZoLDhunnjrQ== +vuetify@^3.11.0: + version "3.11.4" + resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.11.4.tgz#14e41b7a7b821501039251fdf952cec520e505d9" + integrity sha512-614MKpopMzNjkQVD5CS/xr77p4CqtF2wNqQByAi7oTEcKzA+gOsy5O3l97ZIqomNidOSGXyVg5s6t5bNO9p4+Q== w3c-keyname@^2.2.0: version "2.2.8" diff --git a/modoboa/calendars/backends/caldav_.py b/modoboa/calendars/backends/caldav_.py index 17d52331d..4526cc3e7 100644 --- a/modoboa/calendars/backends/caldav_.py +++ b/modoboa/calendars/backends/caldav_.py @@ -80,17 +80,15 @@ class Caldav_Backend(CalendarBackend): def create_event(self, data): """Create a new event.""" uid = uuid.uuid4() - cal = vobject.iCalendar() - cal.add("vevent") - cal.vevent.add("uid").value = str(uid) - cal.vevent.add("summary").value = data["title"] if not data["allDay"]: - cal.vevent.add("dtstart").value = data["start"] - cal.vevent.add("dtend").value = data["end"] + dtstart = data["start"] + dtend = data["end"] else: - cal.vevent.add("dtstart").value = data["start"].date() - cal.vevent.add("dtend").value = data["end"].date() - self.remote_cal.add_event(cal) + dtstart = data["start_date"] + dtend = data["end_date"] + self.remote_cal.save_event( + uid=uid, dtstart=dtstart, dtend=dtend, summary=data["title"] + ) return uid def update_event(self, uid, original_data): @@ -102,8 +100,8 @@ class Caldav_Backend(CalendarBackend): if "title" in data: orig_evt.summary.value = data["title"] if data.get("allDay"): - data["start"] = data["start"].date() - data["end"] = data["end"].date() + data["start"] = data["start_date"] + data["end"] = data["end_date"] if "start" in data: del orig_evt.contents["dtstart"] orig_evt.add("dtstart").value = data["start"] diff --git a/modoboa/calendars/serializers.py b/modoboa/calendars/serializers.py index 5bec9d239..f6f715c37 100644 --- a/modoboa/calendars/serializers.py +++ b/modoboa/calendars/serializers.py @@ -116,11 +116,11 @@ class EventSerializer(serializers.Serializer): id = serializers.CharField(read_only=True) title = serializers.CharField() - start = serializers.DateTimeField() - end = serializers.DateTimeField() + start = serializers.DateTimeField(required=False) + end = serializers.DateTimeField(required=False) allDay = serializers.BooleanField(default=False) color = serializers.CharField(read_only=True) - description = serializers.CharField(required=False) + description = serializers.CharField(required=False, allow_blank=True) attendees = AttendeeSerializer(many=True, required=False) @@ -147,10 +147,13 @@ class WritableEventSerializer(EventSerializer): ) new_calendar_type = serializers.CharField(required=False) + start_date = serializers.DateField(required=False) + end_date = serializers.DateField(required=False) + def __init__(self, *args, **kwargs): """Set calendar list.""" calendar_type = kwargs.pop("calendar_type") - super(EventSerializer, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.update_calendar_field(calendar_type) def update_calendar_field(self, calendar_type): @@ -170,11 +173,13 @@ class WritableEventSerializer(EventSerializer): def validate(self, data): """Make sure dates are present with allDay flag.""" errors = {} - if "allDay" in data: - if "start" not in data: - errors["start"] = _("This field is required.") - if "end" not in data: - errors["end"] = _("This field is required.") + if data.get("allDay", False): + mandatory_fields = ["start_date", "end_date"] + else: + mandatory_fields = ["start", "end"] + for field in mandatory_fields: + if not data.get(field): + errors[field] = _("This field is required.") if errors: raise serializers.ValidationError(errors) return data diff --git a/modoboa/calendars/viewsets.py b/modoboa/calendars/viewsets.py index b0ac8c387..fd89c31de 100644 --- a/modoboa/calendars/viewsets.py +++ b/modoboa/calendars/viewsets.py @@ -21,7 +21,9 @@ from . import serializers def parse_date_from_iso(value): """Return a tz aware datetime parsed from an ISO date.""" - return dateutil.parser.parse(value) + result = dateutil.parser.parse(value) + print(result) + return result class CheckTokenMixin: From 65864d75f4d80f660d0e207c51b89a84d31abfc2 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 12 Jan 2026 12:07:40 +0100 Subject: [PATCH 2/4] Removed references to Fullcalendar --- docker/radicale_config/config | 2 + frontend/package.json | 5 - frontend/src/views/calendars/CalendarView.vue | 136 ++++++++---------- frontend/yarn.lock | 34 ----- modoboa/calendars/backends/caldav_.py | 12 +- 5 files changed, 70 insertions(+), 119 deletions(-) diff --git a/docker/radicale_config/config b/docker/radicale_config/config index c8934b2b5..3d513f2b6 100644 --- a/docker/radicale_config/config +++ b/docker/radicale_config/config @@ -153,6 +153,8 @@ type = none level = debug +request_content_on_debug = true + [headers] # Additional HTTP headers diff --git a/frontend/package.json b/frontend/package.json index 5f8274f7c..c1193ff4a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,11 +15,6 @@ "gettext:compile": "vue-gettext-compile" }, "dependencies": { - "@fullcalendar/core": "^6.1.15", - "@fullcalendar/daygrid": "^6.1.15", - "@fullcalendar/interaction": "^6.1.15", - "@fullcalendar/timegrid": "^6.1.15", - "@fullcalendar/vue3": "^6.1.15", "@mdi/font": "^7.3.67", "@tiptap/extension-highlight": "^3.11.0", "@tiptap/extension-text-align": "^3.11.0", diff --git a/frontend/src/views/calendars/CalendarView.vue b/frontend/src/views/calendars/CalendarView.vue index 9d1ea2bb1..d9b6d44c7 100644 --- a/frontend/src/views/calendars/CalendarView.vue +++ b/frontend/src/views/calendars/CalendarView.vue @@ -68,11 +68,13 @@ :event-ripple="false" @mousedown:event="startDrag" @mousedown:time="startTime" - @click:event="eventClickCallback" + @mousedown:day="startDay" @click:day="dayClickCallback" @mouseleave="cancelDrag" @mousemove:time="mouseMove" + @mousemove:day="mouseMoveDay" @mouseup:time="endDrag" + @mouseup:day="endDragDay" @change="fetchUserEvents" >