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/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 51b6ae5cb..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", @@ -41,7 +36,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..d9b6d44c7 100644 --- a/frontend/src/views/calendars/CalendarView.vue +++ b/frontend/src/views/calendars/CalendarView.vue @@ -1,43 +1,130 @@ + + diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6b8f1b821..39477aa68 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -245,35 +245,6 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== -"@fullcalendar/core@^6.1.15": - version "6.1.19" - resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-6.1.19.tgz#60379111ba0cfb9fd9bc6594fee47cf855fc77b2" - integrity sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ== - dependencies: - preact "~10.12.1" - -"@fullcalendar/daygrid@^6.1.15", "@fullcalendar/daygrid@~6.1.19": - version "6.1.19" - resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-6.1.19.tgz#1d70a7f3f4cc13d3f27f55ed6880aae09a1a367b" - integrity sha512-IAAfnMICnVWPjpT4zi87i3FEw0xxSza0avqY/HedKEz+l5MTBYvCDPOWDATpzXoLut3aACsjktIyw9thvIcRYQ== - -"@fullcalendar/interaction@^6.1.15": - version "6.1.19" - resolved "https://registry.yarnpkg.com/@fullcalendar/interaction/-/interaction-6.1.19.tgz#48d85601a4203ede7cb400323fe5a9c163e28804" - integrity sha512-GOciy79xe8JMVp+1evAU3ytdwN/7tv35t5i1vFkifiuWcQMLC/JnLg/RA2s4sYmQwoYhTw/p4GLcP0gO5B3X5w== - -"@fullcalendar/timegrid@^6.1.15": - version "6.1.19" - resolved "https://registry.yarnpkg.com/@fullcalendar/timegrid/-/timegrid-6.1.19.tgz#bb4b2e3039c995b61284ee5b76ad9dcc696b71ce" - integrity sha512-OuzpUueyO9wB5OZ8rs7TWIoqvu4v3yEqdDxZ2VcsMldCpYJRiOe7yHWKr4ap5Tb0fs7Rjbserc/b6Nt7ol6BRg== - dependencies: - "@fullcalendar/daygrid" "~6.1.19" - -"@fullcalendar/vue3@^6.1.15": - version "6.1.19" - resolved "https://registry.yarnpkg.com/@fullcalendar/vue3/-/vue3-6.1.19.tgz#43cecccfaca27679823b98c73ba2e042c60e98cb" - integrity sha512-j5eUSxx0xIy3ADljo0f5B9PhjqXnCQ+7nUMPfsslc2eGVjp4F74YvY3dyd6OBbg13IvpsjowkjncGipYMQWmTA== - "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" @@ -1936,11 +1907,6 @@ postcss@^8.5.3, postcss@^8.5.6: picocolors "^1.1.1" source-map-js "^1.2.1" -preact@~10.12.1: - version "10.12.1" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21" - integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -2449,10 +2415,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..34f06f31b 100644 --- a/modoboa/calendars/backends/caldav_.py +++ b/modoboa/calendars/backends/caldav_.py @@ -6,6 +6,7 @@ import uuid import caldav from caldav.elements import dav, ical from caldav import Calendar +from dateutil.relativedelta import relativedelta import vobject from django.utils import timezone @@ -53,8 +54,10 @@ class Caldav_Backend(CalendarBackend): start = datetime.datetime.combine( vevent.dtstart.value, datetime.time.min ).replace(tzinfo=tz) + # Small back to make vuetify calendar happy. 'All day' events are generally + # created from one day to the day after (even for a 1 day duration...) end = datetime.datetime.combine( - vevent.dtend.value, datetime.time.min + vevent.dtend.value - relativedelta(days=1), datetime.time.min ).replace(tzinfo=tz) result.update({"allDay": all_day, "start": start, "end": end}) if "attendee" in vevent.contents: @@ -80,17 +83,16 @@ 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"] + # Ensure consistent behavior with other calendar clients (like TB.) + dtend = data["end_date"] + relativedelta(days=1) + 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 +104,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"] @@ -141,7 +143,9 @@ class Caldav_Backend(CalendarBackend): def get_events(self, start, end): """Retrieve a list of events.""" - orig_events = self.remote_cal.search(server_expand=True, start=start, end=end) + orig_events = self.remote_cal.search( + server_expand=False, start=start, end=end, event=True + ) events = [] for event in orig_events: events.append(self._serialize_event(event)) diff --git a/modoboa/calendars/mocks.py b/modoboa/calendars/mocks.py index a06d524b5..702c9ce83 100644 --- a/modoboa/calendars/mocks.py +++ b/modoboa/calendars/mocks.py @@ -49,6 +49,9 @@ class Calendar: def add_event(self, data): return True + def save_event(self, *args, **kwargs): + return True + def event_by_url(self, url): res = objects.Event(url=url, data=EV1, parent=self) return res 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/tests.py b/modoboa/calendars/tests.py index 3f00ed214..5ef224309 100644 --- a/modoboa/calendars/tests.py +++ b/modoboa/calendars/tests.py @@ -428,8 +428,8 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): url = f"/api/v2/user-calendars/{self.calendar.pk}/events/" data = { "title": "Test event", - "start": "2018-03-27T00:00:00Z", - "end": "2018-03-28T00:00:00Z", + "start_date": "2018-03-27", + "end_date": "2018-03-28", "allDay": True, "color": "#ffdddd", "description": "Description", @@ -461,8 +461,8 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/" data = { "title": "Test event", - "start": "2018-03-27T00:00:00Z", - "end": "2018-03-28T00:00:00Z", + "start_date": "2018-03-27", + "end_date": "2018-03-28", "allDay": True, "color": "#ffdddd", "description": "Description", @@ -483,8 +483,8 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/" data = { - "start": "2018-03-27T00:00:00Z", - "end": "2018-03-28T00:00:00Z", + "start_date": "2018-03-27", + "end_date": "2018-03-28", "allDay": True, } response = self.client.patch(url, data=data, format="json") @@ -496,8 +496,8 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase): self.client.post(reverse("core:login"), data) data = { "title": "Test event", - "start": "2018-03-27T00:00:00Z", - "end": "2018-03-28T00:00:00Z", + "start_date": "2018-03-27", + "end_date": "2018-03-28", "allDay": True, "color": "#ffdddd", "description": "Description", 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: