Merge pull request #3851 from modoboa/refacto/vuetify_based_calendar

Replaced FullCalendar by Vuetify internal calendar
This commit is contained in:
Antoine Nguyen
2026-01-12 14:32:03 +01:00
committed by GitHub
11 changed files with 515 additions and 232 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ LABEL org.opencontainers.image.authors="Antoine Nguyen <tonio@ngyn.org>"
WORKDIR /app
RUN pip install Radicale pytz radicale-modoboa-auth-oauth2
RUN pip install -U Radicale pytz radicale-modoboa-auth-oauth2
EXPOSE 5232
+2
View File
@@ -153,6 +153,8 @@ type = none
level = debug
request_content_on_debug = true
[headers]
# Additional HTTP headers
+1 -6
View File
@@ -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",
+36 -14
View File
@@ -26,7 +26,7 @@
:error-messages="formErrors.start"
density="compact"
variant="outlined"
type="datetime-local"
:type="dateType"
class="mr-4"
/>
<v-text-field
@@ -36,7 +36,7 @@
:error-messages="formErrors.end"
density="compact"
variant="outlined"
type="datetime-local"
:type="dateType"
/>
</div>
<div class="d-flex mb-3">
@@ -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') })
+426 -142
View File
@@ -1,43 +1,130 @@
<template>
<div class="text-h5 ml-4 mb-6">
{{ $gettext('Calendar') }}
</div>
<FullCalendar ref="calendarRef" :options="calendarOptions" />
<div style="display: grid; grid-template-rows: auto auto 1fr; height: 96vh">
<div class="text-h5 ml-4 mb-6">
{{ $gettext('Calendar') }}
</div>
<v-dialog v-model="showInformation" persistent max-width="800px">
<CalendarDetail :calendar="selectedCalendar" @close="closeInformation" />
</v-dialog>
<v-dialog v-model="showCalendarForm" persistent max-width="800px">
<CalendarForm
:initial-calendar="selectedCalendar"
@color-changed="refreshCalendarEvents"
@close="closeCalendarForm"
/>
</v-dialog>
<v-dialog v-model="showEventForm" persistent max-width="800px">
<EventForm
:info="selectInfo"
:event="selectedEvent"
@refresh-calendar="refreshCalendarEvents"
@close="closeEventForm"
/>
</v-dialog>
<v-dialog v-model="showAccessRulesForm" persistent max-width="800px">
<CalendarAccessRulesForm
v-if="selectedCalendar"
:calendar-pk="selectedCalendar.pk"
@close="closeAccessRulesForm"
/>
</v-dialog>
<v-dialog v-model="showImportEventsForm" persistent max-width="800px">
<ImportEventsForm
v-if="selectedCalendar"
:calendar="selectedCalendar"
@events-imported="refreshCalendarEvents"
@close="closeImportEventsForm"
/>
</v-dialog>
<ConfirmDialog ref="confirm" />
<v-toolbar v-if="calendarRef" color="white" flat>
<v-btn
class="me-4"
color="grey-darken-2"
variant="outlined"
@click="setToday"
>
{{ $gettext('Today') }}
</v-btn>
<v-btn
color="grey-darken-2"
size="small"
variant="text"
icon
@click="goPrev"
>
<v-icon size="small"> mdi-chevron-left </v-icon>
</v-btn>
<v-btn
color="grey-darken-2"
size="small"
variant="text"
icon
@click="goNext"
>
<v-icon size="small"> mdi-chevron-right </v-icon>
</v-btn>
<v-toolbar-title>
{{ calendarRef.title }}
</v-toolbar-title>
<v-menu location="bottom end">
<template #activator="{ props }">
<v-btn color="grey-darken-2" variant="outlined" v-bind="props">
<span>{{ typeToLabel[ctype] }}</span>
<v-icon end> mdi-menu-down </v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="ctype = 'day'">
<v-list-item-title>{{ $gettext('Day') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="ctype = 'week'">
<v-list-item-title>{{ $gettext('Week') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="ctype = 'month'">
<v-list-item-title>{{ $gettext('Month') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
<div style="overflow: auto">
<v-calendar
ref="calendarRef"
v-model="focus"
:events="events"
first-day-of-week="1"
:type="ctype"
:locale="current"
event-name="title"
:event-color="getEventColor"
:event-ripple="false"
@mousedown:event="startDrag"
@mousedown:time="startTime"
@mousedown:day="startDay"
@click:day="dayClickCallback"
@mouseleave="cancelDrag"
@mousemove:time="mouseMove"
@mousemove:day="mouseMoveDay"
@mouseup:time="endDrag"
@mouseup:day="endDragDay"
@change="fetchUserEvents"
>
<template #event="{ event, timed, eventSummary }">
<div class="v-event-draggable">
<component :is="eventSummary"></component>
</div>
<div
v-if="timed"
class="v-event-drag-bottom"
@mousedown.stop="extendBottom(event)"
></div>
</template>
</v-calendar>
</div>
<v-dialog v-model="showInformation" persistent max-width="800px">
<CalendarDetail :calendar="selectedCalendar" @close="closeInformation" />
</v-dialog>
<v-dialog v-model="showCalendarForm" persistent max-width="800px">
<CalendarForm
:initial-calendar="selectedCalendar"
@color-changed="refreshCalendarEvents"
@close="closeCalendarForm"
/>
</v-dialog>
<v-dialog v-model="showEventForm" persistent max-width="800px">
<EventForm
:info="selectInfo"
:event="selectedEvent"
@refresh-calendar="refreshCalendarEvents"
@close="closeEventForm"
/>
</v-dialog>
<v-dialog v-model="showAccessRulesForm" persistent max-width="800px">
<CalendarAccessRulesForm
v-if="selectedCalendar"
:calendar-pk="selectedCalendar.pk"
@close="closeAccessRulesForm"
/>
</v-dialog>
<v-dialog v-model="showImportEventsForm" persistent max-width="800px">
<ImportEventsForm
v-if="selectedCalendar"
:calendar="selectedCalendar"
@events-imported="refreshCalendarEvents"
@close="closeImportEventsForm"
/>
</v-dialog>
<ConfirmDialog ref="confirm" />
</div>
</template>
<script setup>
@@ -53,10 +140,6 @@ import CalendarDetail from '@/components/calendars/CalendarDetail'
import ConfirmDialog from '@/components/tools/ConfirmDialog'
import EventForm from '@/components/calendars/EventForm'
import ImportEventsForm from '@/components/calendars/ImportEventsForm'
import FullCalendar from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction'
import timeGridPlugin from '@fullcalendar/timegrid'
const { $gettext, current } = useGettext()
const layoutStore = useLayoutStore()
@@ -64,6 +147,7 @@ const busStore = useBusStore()
const calendarRef = ref()
const confirm = ref()
const events = ref([])
const selectInfo = ref()
const selectedCalendar = ref(null)
const selectedEvent = ref(null)
@@ -73,6 +157,22 @@ const showCalendarForm = ref(false)
const showInformation = ref(false)
const showEventForm = ref(false)
const showImportEventsForm = ref(false)
const ctype = ref('week')
const focus = ref('')
const dragEvent = ref(null)
const dragTime = ref(null)
const dragDay = ref(null)
const createEvent = ref(null)
const createStart = ref(null)
const extendOriginal = ref(null)
const updating = ref(false)
const moving = ref(false)
const typeToLabel = {
day: $gettext('Day'),
week: $gettext('Week'),
month: $gettext('Month'),
}
const leftMenuItems = computed(() => {
const result = [
@@ -121,21 +221,36 @@ const leftMenuItems = computed(() => {
return result
})
async function fetchUserCalendarEvents(
calendarPk,
info,
successCallback,
failureCallback
) {
try {
const resp = await api.getUserCalendarEvents(calendarPk, {
start: info.startStr,
end: info.endStr,
async function fetchUserEvents({ start, end }) {
let newEvents = []
for (const calendar of userCalendars.value) {
const resp = await api.getUserCalendarEvents(calendar.pk, {
start: start.date,
end: end.date,
})
successCallback(resp.data)
} catch (err) {
failureCallback(err)
newEvents = newEvents.concat(
resp.data.map((event) => {
const newEvent = { ...event }
newEvent.start = Date.parse(event.start)
newEvent.end = Date.parse(event.end)
newEvent.timed = !event.allDay
return newEvent
})
)
}
events.value = newEvents
}
const setToday = () => {
focus.value = ''
}
const goPrev = () => {
calendarRef.value.prev()
}
const goNext = () => {
calendarRef.value.next()
}
function openCalendarForm() {
@@ -178,58 +293,231 @@ function closeImportEventsForm() {
showImportEventsForm.value = false
}
function fetchUserCalendars() {
api.getUserCalendars().then((resp) => {
userCalendars.value = resp.data
async function fetchUserCalendars() {
const resp = await api.getUserCalendars()
userCalendars.value = resp.data
}
function refreshCalendarEvents() {
fetchUserEvents({
start: calendarRef.value.renderProps.start,
end: calendarRef.value.renderProps.end,
})
}
function refreshCalendarEvents(calendarPk) {
const fullc = calendarRef.value.getApi()
const evtSource = fullc.getEventSourceById(`user-${calendarPk}`)
evtSource.refetch()
}
function selectCallback(info) {
selectInfo.value = info
showEventForm.value = true
}
async function eventClickCallback(info) {
const resp = await api.getUserEvent(
info.event.extendedProps.calendar.pk,
info.event.id
)
selectedEvent.value = resp.data
showEventForm.value = true
}
function updateEventDates(calEvent) {
var data = {
start: calEvent.start,
end: calEvent.end,
const dayClickCallback = (nativeEvent, { date }) => {
if (!selectedEvent.value) {
const startDate = new Date(date)
const endDate = new Date(date)
selectInfo.value = {
color: '#2196F3',
start: startDate,
end: endDate,
timed: false,
}
showEventForm.value = true
}
const evtCalendar = calEvent.extendedProps.calendar
if (calEvent.allDay) {
const end = DateTime.fromJSDate(data.start)
end.plus({ days: 1 })
data.start.setHours(0, 0, 0)
data.end = end
}
async function updateEventDates(calEvent) {
updating.value = true
var data = {
start: DateTime.fromMillis(calEvent.start).toJSDate(),
end: calEvent.end ? DateTime.fromMillis(calEvent.end).toJSDate() : null,
}
const evtCalendar = calEvent.calendar
if (!calEvent.timed) {
data.start_date = data.start.toISOString().split('T')[0]
data.end_date = data.end.toISOString().split('T')[0]
data.allDay = calEvent.allDay
} else if (!calEvent.end) {
const end = DateTime.fromJSDate(data.start)
end.plus({ hours: 1 })
data.end = end
}
api.patchUserEvent(evtCalendar.pk, calEvent.id, data).then((response) => {
try {
await api.patchUserEvent(evtCalendar.pk, calEvent.id, data)
busStore.displayNotification({ msg: $gettext('Event updated') })
})
} finally {
updating.value = false
}
}
function eventDropCallback(info) {
updateEventDates(info.event)
const roundTime = (time, down = true) => {
const roundTo = 15 // minutes
const roundDownTime = roundTo * 60 * 1000
return down
? time - (time % roundDownTime)
: time + (roundDownTime - (time % roundDownTime))
}
function eventResizeCallback(info) {
updateEventDates(info.event)
const toTime = (tms) => {
return new Date(
tms.year,
tms.month - 1,
tms.day,
tms.hour,
tms.minute
).getTime()
}
const getEventColor = (event) => {
const rgb = parseInt(event.color.substring(1), 16)
const r = (rgb >> 16) & 0xff
const g = (rgb >> 8) & 0xff
const b = (rgb >> 0) & 0xff
return event === dragEvent.value
? `rgba(${r}, ${g}, ${b}, 0.7)`
: event === createEvent.value
? `rgba(${r}, ${g}, ${b}, 0.7)`
: event.color
}
const startDrag = (nativeEvent, { event, timed }) => {
if (event) {
dragEvent.value = event
if (timed) {
dragTime.value = null
extendOriginal.value = null
} else {
dragDay.value = null
}
}
}
const startTime = (nativeEvent, tms) => {
const mouse = toTime(tms)
if (dragEvent.value && dragTime.value === null) {
const start = dragEvent.value.start
dragTime.value = mouse - start
} else {
createStart.value = roundTime(mouse)
createEvent.value = {
title: $gettext('New event'),
color: '#2196F3',
start: createStart.value,
end: createStart.value,
timed: true,
}
events.value.push(createEvent.value)
}
}
const startDay = (nativeEvent, tms) => {
const mouse = toTime(tms)
if (dragEvent.value && dragDay.value === null) {
dragDay.value = mouse - dragEvent.value.start
}
}
const mouseMove = (nativeEvent, tms) => {
const mouse = toTime(tms)
if (dragEvent.value && dragTime.value !== null) {
moving.value = true
const start = dragEvent.value.start
const end = dragEvent.value.end
const duration = end - start
const newStartTime = mouse - dragTime.value
const newStart = roundTime(newStartTime)
const newEnd = newStart + duration
dragEvent.value.start = newStart
dragEvent.value.end = newEnd
} else if (createEvent.value && createStart.value !== null) {
moving.value = true
const mouseRounded = roundTime(mouse, false)
const min = Math.min(mouseRounded, createStart.value)
const max = Math.max(mouseRounded, createStart.value)
createEvent.value.start = min
createEvent.value.end = max
}
}
const mouseMoveDay = (nativeEvent, tms) => {
const mouse = toTime(tms)
if (dragEvent.value && dragDay.value !== null) {
moving.value = true
const start = dragEvent.value.start
const end = dragEvent.value.end
const duration = end - start
const newStartTime = mouse - dragDay.value
const newStart = roundTime(newStartTime)
const newEnd = newStart + duration
dragEvent.value.start = newStart
dragEvent.value.end = newEnd
}
}
const extendBottom = (event) => {
createEvent.value = event
createStart.value = event.start
extendOriginal.value = event.end
}
const endDrag = () => {
if (createEvent.value && !extendOriginal.value) {
selectInfo.value = createEvent.value
showEventForm.value = true
} else {
if (moving.value) {
if (dragEvent.value) {
updateEventDates(dragEvent.value)
} else if (createEvent.value) {
updateEventDates(createEvent.value)
}
} else {
selectedEvent.value = dragEvent.value
showEventForm.value = true
}
dragTime.value = null
dragEvent.value = null
createEvent.value = null
createStart.value = null
extendOriginal.value = null
moving.value = false
}
}
const endDragDay = () => {
if (dragEvent.value) {
if (moving.value) {
updateEventDates(dragEvent.value)
} else {
selectedEvent.value = dragEvent.value
showEventForm.value = true
}
}
dragEvent.value = null
dragDay.value = null
moving.value = false
}
const cancelDrag = () => {
if (createEvent.value) {
if (extendOriginal.value) {
createEvent.value.end = extendOriginal.value
} else {
const i = events.value.indexOf(createEvent.value)
if (i !== -1) {
events.value.splice(i, 1)
}
}
}
createEvent.value = null
createStart.value = null
dragTime.value = null
dragEvent.value = null
moving.value = false
}
function closeEventForm() {
@@ -252,40 +540,12 @@ async function deleteCalendar(calendarPk) {
return
}
await api.deleteUserCalendar(calendarPk)
const fullc = calendarRef.value.getApi()
const evtSource = fullc.getEventSourceById(`user-${calendarPk}`)
evtSource.remove()
fetchUserCalendars()
await fetchUserCalendars()
events.value = []
calendarRef.value.checkChange()
busStore.displayNotification({ msg: $gettext('Calendar deleted') })
}
const calendarOptions = {
plugins: [timeGridPlugin, dayGridPlugin, interactionPlugin],
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay',
},
scrollTime: '09:00:00',
height: '100%',
firstDay: 1,
initialView: 'timeGridWeek',
locale: current,
selectable: true,
selectMirror: true,
editable: true,
dayMaxEventRows: true,
businessHours: {
daysOfWeek: [1, 2, 3, 4, 5],
startTime: '09:00',
endTime: '18:00',
},
select: selectCallback,
eventClick: eventClickCallback,
eventDrop: eventDropCallback,
eventResize: eventResizeCallback,
}
watch(
leftMenuItems,
(value) => {
@@ -294,23 +554,47 @@ watch(
{ immediate: true }
)
watch(userCalendars, (value) => {
const fullc = calendarRef.value.getApi()
if (!fullc) {
return
}
for (const calendar of value) {
const evtSource = fullc.getEventSourceById(`user-${calendar.pk}`)
if (!evtSource) {
fullc.addEventSource({
id: `user-${calendar.pk}`,
events: fetchUserCalendarEvents.bind(this, calendar.pk),
})
}
}
onMounted(() => {
calendarRef.value.scrollToTime('07:00')
calendarRef.value.checkChange()
})
onMounted(() => {
fetchUserCalendars()
})
await fetchUserCalendars()
</script>
<style scoped>
.v-event-draggable {
padding-left: 6px;
}
.v-event-timed {
user-select: none;
-webkit-user-select: none;
}
.v-event-drag-bottom {
position: absolute;
left: 0;
right: 0;
bottom: 4px;
height: 4px;
cursor: ns-resize;
&::after {
display: none;
position: absolute;
left: 50%;
height: 4px;
border-top: 1px solid white;
border-bottom: 1px solid white;
width: 16px;
margin-left: -8px;
opacity: 0.8;
content: '';
}
&:hover::after {
display: block;
}
}
</style>
+4 -38
View File
@@ -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"
+17 -13
View File
@@ -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))
+3
View File
@@ -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
+14 -9
View File
@@ -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
+8 -8
View File
@@ -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",
+3 -1
View File
@@ -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: