caldav_sync: recurrence working from Odoo to CalDAV. Need further testing for the other direction.
This commit is contained in:
parent
ce7b27cc65
commit
5ca1d56e54
2 changed files with 155 additions and 141 deletions
|
|
@ -6,8 +6,8 @@ from odoo import models, api, fields
|
||||||
from odoo.addons.calendar.models.calendar_recurrence import MAX_RECURRENT_EVENT
|
from odoo.addons.calendar.models.calendar_recurrence import MAX_RECURRENT_EVENT
|
||||||
import caldav
|
import caldav
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, date
|
from datetime import datetime
|
||||||
from icalendar import vCalAddress, vText, vDatetime, vRecur, Event, vInt
|
from icalendar import vCalAddress, vText, vDatetime, vRecur, Event
|
||||||
import re
|
import re
|
||||||
from pytz import timezone, utc
|
from pytz import timezone, utc
|
||||||
from typing import List, Dict, TypeVar, Optional
|
from typing import List, Dict, TypeVar, Optional
|
||||||
|
|
@ -87,22 +87,10 @@ class CalendarEvent(models.Model):
|
||||||
#### Field Computation Methods ####
|
#### Field Computation Methods ####
|
||||||
###################################
|
###################################
|
||||||
|
|
||||||
def _recompute_caldav_uid(self):
|
def _recompute_caldav_ids(self):
|
||||||
for event in self:
|
for event in self:
|
||||||
_logger.info(f"Recomputing CalDAV UID from {event.caldav_uid}")
|
|
||||||
if event.recurrence_id:
|
if event.recurrence_id:
|
||||||
event.caldav_uid = event.recurrence_id.caldav_uid
|
event.caldav_uid = event.recurrence_id.caldav_uid
|
||||||
else:
|
|
||||||
event.caldav_uid = uuid.uuid4()
|
|
||||||
_logger.info(f"New CalDAV UID: {event.caldav_uid}")
|
|
||||||
|
|
||||||
@api.depends("recurrence_id", "recurrence_id.dtstart")
|
|
||||||
def _recompute_caldav_recurrence_id(self):
|
|
||||||
for event in self:
|
|
||||||
_logger.info(
|
|
||||||
f"Recomputing caldav_recurrence_id for event {event} at {event.start}"
|
|
||||||
)
|
|
||||||
if event.recurrence_id:
|
|
||||||
time = event.recurrence_id.dtstart.time()
|
time = event.recurrence_id.dtstart.time()
|
||||||
date = event.start.date()
|
date = event.start.date()
|
||||||
event.caldav_recurrence_id = datetime(
|
event.caldav_recurrence_id = datetime(
|
||||||
|
|
@ -114,6 +102,7 @@ class CalendarEvent(models.Model):
|
||||||
time.second,
|
time.second,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
event.caldav_uid = uuid.uuid4()
|
||||||
event.caldav_recurrence_id = False
|
event.caldav_recurrence_id = False
|
||||||
|
|
||||||
@api.depends("name", "description", "partner_ids", "location", "videocall_location")
|
@api.depends("name", "description", "partner_ids", "location", "videocall_location")
|
||||||
|
|
@ -192,8 +181,7 @@ class CalendarEvent(models.Model):
|
||||||
CalendarEvent, self.with_context({"caldav_no_sync": True})
|
CalendarEvent, self.with_context({"caldav_no_sync": True})
|
||||||
).create(vals_list)
|
).create(vals_list)
|
||||||
ctx = {"caldav_no_sync": True}
|
ctx = {"caldav_no_sync": True}
|
||||||
events.with_context(ctx)._recompute_caldav_uid()
|
events.with_context(ctx)._recompute_caldav_ids()
|
||||||
events.with_context(ctx)._recompute_caldav_recurrence_id()
|
|
||||||
if not self.env.context.get("caldav_no_sync"):
|
if not self.env.context.get("caldav_no_sync"):
|
||||||
events._to_sync()._sync_create_to_caldav()
|
events._to_sync()._sync_create_to_caldav()
|
||||||
return events
|
return events
|
||||||
|
|
@ -203,17 +191,15 @@ class CalendarEvent(models.Model):
|
||||||
for user in event.caldav_user_ids:
|
for user in event.caldav_user_ids:
|
||||||
client = user._get_caldav_client()
|
client = user._get_caldav_client()
|
||||||
calendar = client.calendar(url=user.caldav_calendar_url)
|
calendar = client.calendar(url=user.caldav_calendar_url)
|
||||||
# try:
|
try:
|
||||||
_logger.info(f"Creating new CalDAV event for {event.name}")
|
caldav_events = event._create_in_icalendar(calendar)
|
||||||
caldav_events = event._create_in_icalendar(calendar)
|
for caldav_event in caldav_events:
|
||||||
for caldav_event in caldav_events:
|
caldav_uid = caldav_event.vobject_instance.vevent.uid.value
|
||||||
caldav_uid = caldav_event.vobject_instance.vevent.uid.value
|
event.with_context(caldav_no_sync=True).write(
|
||||||
_logger.info(f"New CalDAV UID: {caldav_uid}")
|
{"caldav_uid": caldav_uid}
|
||||||
event.with_context(caldav_no_sync=True).write(
|
)
|
||||||
{"caldav_uid": caldav_uid}
|
except Exception as e:
|
||||||
)
|
_logger.error(f"Failed to sync event to CalDAV server: {e}")
|
||||||
# except Exception as e:
|
|
||||||
# _logger.error(f"Failed to sync event to CalDAV server: {e}")
|
|
||||||
|
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
res = super(CalendarEvent, self.with_context({"caldav_no_sync": True})).write(
|
res = super(CalendarEvent, self.with_context({"caldav_no_sync": True})).write(
|
||||||
|
|
@ -228,7 +214,6 @@ class CalendarEvent(models.Model):
|
||||||
to_sync = self._to_sync()
|
to_sync = self._to_sync()
|
||||||
if to_sync and not self.env.context.get("caldav_no_sync"):
|
if to_sync and not self.env.context.get("caldav_no_sync"):
|
||||||
for rec in to_sync:
|
for rec in to_sync:
|
||||||
_logger.info(f"Updating event {rec.name} at {rec.start} in " f"CalDAV.")
|
|
||||||
rec._sync_write_to_caldav()
|
rec._sync_write_to_caldav()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
@ -238,84 +223,159 @@ class CalendarEvent(models.Model):
|
||||||
client = user._get_caldav_client()
|
client = user._get_caldav_client()
|
||||||
calendar = client.calendar(url=user.caldav_calendar_url)
|
calendar = client.calendar(url=user.caldav_calendar_url)
|
||||||
|
|
||||||
event = None
|
base_event = self._get_caldav_base_event_by_uid(calendar, self.caldav_uid)
|
||||||
try:
|
if self.recurrence_id:
|
||||||
event = self._find_in_caldav_calendar(calendar)
|
tz = timezone(self.event_tz or self.env.user.tz)
|
||||||
except caldav.error.NotFoundError:
|
start = utc.localize(self.start).astimezone(tz)
|
||||||
_logger.info(
|
# We have an event in the recurrence chain that differs from the base
|
||||||
f"CalDAV event {self.caldav_uid} not found on server during update."
|
# We need to add an iCalendar VEVENT with recurrence-id or update it
|
||||||
f" Creating it instead."
|
if self.is_base_event and start == self._get_ical_recurrence_id():
|
||||||
)
|
# We have a normal base event, so we just update it or create it
|
||||||
if event:
|
self._update_base_caldav_event(
|
||||||
event.save()
|
calendar, base_event, ical_event_data
|
||||||
elif self.recurrence_id and not self.is_base_event:
|
)
|
||||||
base_event = self._get_caldav_base_event_by_uid(
|
elif (
|
||||||
calendar, self.caldav_uid
|
self.differs_from_base_event
|
||||||
)
|
or start != self._get_ical_recurrence_id()
|
||||||
base_event.icalendar_instance.add_component(Event(**ical_event_data))
|
):
|
||||||
if self.caldav_recurrence_id != self.start:
|
# We have an event that differs from the base event in some way
|
||||||
tz = timezone(self.event_tz)
|
# So we need to update or create its matching subcomponent, identified
|
||||||
base_event.save()
|
# by its recurrence-id
|
||||||
|
if not base_event:
|
||||||
|
_logger.warning(
|
||||||
|
f"Failed to find base event for {self} on CalDAV server."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
index = self._get_subcomponent_index_for_recurrence(base_event)
|
||||||
|
if index:
|
||||||
|
ical_event = base_event.icalendar_instance.subcomponents[index]
|
||||||
|
self._update_ical_event_values(ical_event, ical_event_data)
|
||||||
|
else:
|
||||||
|
base_event.icalendar_instance.add_component(
|
||||||
|
Event(**ical_event_data)
|
||||||
|
)
|
||||||
|
base_event.save()
|
||||||
else:
|
else:
|
||||||
calendar.add_event(**ical_event_data)
|
self._update_base_caldav_event(calendar, base_event, ical_event_data)
|
||||||
|
|
||||||
|
def _update_base_caldav_event(
|
||||||
|
self,
|
||||||
|
calendar: icalendar.cal.Calendar,
|
||||||
|
event: caldav.Event,
|
||||||
|
ical_event_data: dict,
|
||||||
|
):
|
||||||
|
if event:
|
||||||
|
self._update_ical_event_values(event.icalendar_component, ical_event_data)
|
||||||
|
event.save()
|
||||||
|
else:
|
||||||
|
calendar.add_event(**ical_event_data)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _update_ical_event_values(
|
||||||
|
self, ical_event: icalendar.cal.Event, event_data: dict
|
||||||
|
):
|
||||||
|
for key, value in event_data.items():
|
||||||
|
ical_event[key] = value
|
||||||
|
|
||||||
def _get_caldav_base_event_by_uid(
|
def _get_caldav_base_event_by_uid(
|
||||||
self, calendar: caldav.Calendar, uid: str
|
self, calendar: caldav.Calendar, uid: str
|
||||||
) -> Optional[CalendarEvent]:
|
) -> Optional[CalendarEvent]:
|
||||||
for event in calendar.events():
|
for event in calendar.events():
|
||||||
comp = event.icalendar_component
|
component = event.icalendar_component
|
||||||
event_uid = self._extract_component_text(comp, "uid")
|
event_uid = self._extract_component_text(component, "uid")
|
||||||
event_recurrence_id = self._extract_component_text(comp, "recurrence-id")
|
if event_uid == uid and not component.get("recurrence-id"):
|
||||||
if event_uid == uid and not event_recurrence_id:
|
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def unlink(self):
|
def unlink(self):
|
||||||
if not self.env.context.get("caldav_no_sync"):
|
if not self.env.context.get("caldav_no_sync"):
|
||||||
for rec in self._to_sync():
|
for rec in self._to_sync():
|
||||||
try:
|
try:
|
||||||
# TODO: fix the attempt to sync all events ... _to_sync is
|
|
||||||
# not working here
|
|
||||||
_logger.info(f"Removing event {rec.name} from CalDAV")
|
|
||||||
rec._sync_unlink_to_caldav()
|
rec._sync_unlink_to_caldav()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.error(f"Failed to delete event from CalDAV server: {e}")
|
_logger.error(f"Failed to delete event from CalDAV server: {e}")
|
||||||
return super(CalendarEvent, self).unlink()
|
return super(CalendarEvent, self).unlink()
|
||||||
|
|
||||||
|
def _get_subcomponent_index_for_recurrence(
|
||||||
|
self, caldav_event: caldav.Event
|
||||||
|
) -> Optional[int]:
|
||||||
|
ical_instance = caldav_event.icalendar_instance
|
||||||
|
for index, component in enumerate(ical_instance.subcomponents):
|
||||||
|
if component.get("name") == "VEVENT" and (
|
||||||
|
rec_id := component.get("recurrence-id")
|
||||||
|
and rec_id.dt == self._get_ical_recurrence_id()
|
||||||
|
):
|
||||||
|
return index
|
||||||
|
|
||||||
def _sync_unlink_to_caldav(self):
|
def _sync_unlink_to_caldav(self):
|
||||||
if self.caldav_uid:
|
if self.caldav_uid:
|
||||||
for user in self.caldav_user_ids:
|
for user in self.caldav_user_ids:
|
||||||
client = user._get_caldav_client()
|
client = user._get_caldav_client()
|
||||||
calendar = client.calendar(url=user.caldav_calendar_url)
|
calendar = client.calendar(url=user.caldav_calendar_url)
|
||||||
try:
|
try:
|
||||||
_logger.info(
|
caldav_event = calendar.event_by_uid(self.caldav_uid)
|
||||||
f"Removing CalDAV event {self.caldav_uid}, Odoo ID {self.id} starting at {self.start}"
|
if self.recurrence_id and not self.is_base_event:
|
||||||
)
|
index = self._get_subcomponent_index_for_recurrence(
|
||||||
caldav_event = self._find_in_caldav_calendar(
|
caldav_event
|
||||||
calendar, match_start=True
|
|
||||||
)
|
|
||||||
if caldav_event:
|
|
||||||
_logger.info(
|
|
||||||
f"Found event {caldav_event.icalendar_component.get("uid")}{caldav_event.icalendar_component.get("dtstart").dt}"
|
|
||||||
)
|
)
|
||||||
caldav_event.delete()
|
if index:
|
||||||
|
caldav_event.icalendar_instance.subcomponents.pop(index)
|
||||||
|
caldav_event.save()
|
||||||
|
else:
|
||||||
|
# In some cases, like when events are detached from a recurrence,
|
||||||
|
# they can be base events but have an old caldav_uid from when
|
||||||
|
# they were recurring. That's why we make sure that the start
|
||||||
|
# of the event matches.
|
||||||
|
if self._matches_caldav_start(caldav_event):
|
||||||
|
caldav_event.delete()
|
||||||
except caldav.error.NotFoundError:
|
except caldav.error.NotFoundError:
|
||||||
_logger.info(
|
# No worries - it just didn't exist so nothing to sync
|
||||||
f"CalDAV event {self.caldav_uid} starting at {self.start}"
|
pass
|
||||||
f" not found on server during deletion."
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.error(f"Failed to remove event from CalDAV server: {e}")
|
_logger.error(f"Failed to remove event from CalDAV server: {e}")
|
||||||
|
|
||||||
def _sync_recurrence_to_caldav(self):
|
def _matches_caldav_start(self, caldav_event: caldav.Event) -> bool:
|
||||||
for event in self._to_sync():
|
event_start = caldav_event.icalendar_component.get("dtstart").dt
|
||||||
|
tz = event_start.tzinfo
|
||||||
|
self_start = utc.localize(self.start).astimezone(tz)
|
||||||
|
return self_start == event_start
|
||||||
|
|
||||||
|
def _update_future_events(self, values, time_values, recurrence_values):
|
||||||
|
"""When self makes a change updating future events in the recurrence, we need
|
||||||
|
only to make sure that self is properly synchronized after the change. We also
|
||||||
|
need to make sure that the recurrence rule on the old base event is re-synced.
|
||||||
|
The superclass method already archives all the future events and re-creates
|
||||||
|
them, so we implicitly delete them from the CalDAV server before writing the
|
||||||
|
new base event (self).
|
||||||
|
|
||||||
|
We do, however, need to get a new caldav_uid and caldav_recurrence_id for all
|
||||||
|
the events in the new chain.
|
||||||
|
"""
|
||||||
|
old_base = self.recurrence_id.base_event_id
|
||||||
|
super()._update_future_events(values, time_values, recurrence_values)
|
||||||
|
events_to_refresh = self.recurrence_id._get_events_from(self.start)
|
||||||
|
events_to_refresh._recompute_caldav_ids()
|
||||||
|
self._sync_write_to_caldav()
|
||||||
|
old_base._sync_write_to_caldav()
|
||||||
|
|
||||||
|
def _break_recurrence(self, future=True):
|
||||||
|
recurrence = self.recurrence_id
|
||||||
|
detached_events = super()._break_recurrence(future) | self
|
||||||
|
# detached_events are already removed from the CalDAV server in
|
||||||
|
# calendar.recurrence._detach_events. We now need to reinitialize them.
|
||||||
|
for event in detached_events:
|
||||||
event._sync_write_to_caldav()
|
event._sync_write_to_caldav()
|
||||||
|
if future:
|
||||||
|
# When future=True, the base event needs to re-sync its recurrence values
|
||||||
|
recurrence.base_event_id._sync_write_to_caldav()
|
||||||
|
return detached_events - self
|
||||||
|
|
||||||
def _to_sync(self):
|
def _to_sync(self):
|
||||||
"""Determine which records in self we need to synchronize with the
|
"""Determine which records in self we need to synchronize with the
|
||||||
CalDAV server. In essence, we only synchronize base events and those
|
CalDAV server. In essence, we only synchronize base events and those
|
||||||
differing from the base event in a recurring chain."""
|
differing from the base event in a recurring chain."""
|
||||||
return self.filtered(
|
return self.filtered(
|
||||||
lambda event: event._is_caldav_enabled()
|
lambda event: event.id
|
||||||
|
and event._is_caldav_enabled()
|
||||||
and (event.is_base_event or event.differs_from_base_event)
|
and (event.is_base_event or event.differs_from_base_event)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -428,51 +488,6 @@ class CalendarEvent(models.Model):
|
||||||
}
|
}
|
||||||
return mapping.get(state, "NEEDS-ACTION")
|
return mapping.get(state, "NEEDS-ACTION")
|
||||||
|
|
||||||
def _find_in_caldav_calendar(
|
|
||||||
self,
|
|
||||||
calendar: caldav.Calendar,
|
|
||||||
force_recurrence_id: bool = False,
|
|
||||||
match_start: bool = False,
|
|
||||||
) -> Optional[caldav.Event]:
|
|
||||||
"""Find this event in a given CalDAV Calendar. If the event is
|
|
||||||
recurring but is not a base event, it will be matched with its
|
|
||||||
caldav_uid and caldav_recurrence_id. Base events are matched by UID
|
|
||||||
only.
|
|
||||||
|
|
||||||
:param calendar: The CalDAV Calendar to find the event in.
|
|
||||||
:param force_recurrence_id: Match with the caldav_recurrence_id even
|
|
||||||
if self is a base event.
|
|
||||||
:return: The CalDAV Event found in the calendar (or None).
|
|
||||||
"""
|
|
||||||
# Since search by uid and recurrence-id is not yet supported,
|
|
||||||
# we do it manually
|
|
||||||
match_recurrence_id = (
|
|
||||||
force_recurrence_id or self.recurrence_id and not self.is_base_event
|
|
||||||
)
|
|
||||||
if not match_recurrence_id and not match_start:
|
|
||||||
calendar.event_by_uid(self.caldav_uid)
|
|
||||||
else:
|
|
||||||
events = calendar.events()
|
|
||||||
for event in events:
|
|
||||||
event_instance = event.icalendar_component
|
|
||||||
if (
|
|
||||||
event_instance.get("uid") == self.caldav_uid
|
|
||||||
and (
|
|
||||||
not match_recurrence_id
|
|
||||||
or (rec_id := event_instance.get("recurrence-id"))
|
|
||||||
and rec_id == self._get_ical_recurrence_id()
|
|
||||||
)
|
|
||||||
and (
|
|
||||||
not match_start
|
|
||||||
or event_instance.get("dtstart").dt
|
|
||||||
== utc.localize(self.start).astimezone(
|
|
||||||
event_instance.get("dtstart").dt.tzinfo
|
|
||||||
)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
return event
|
|
||||||
return None
|
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#### Methods for synchronizing changes from the server to Odoo ####
|
#### Methods for synchronizing changes from the server to Odoo ####
|
||||||
###################################################################
|
###################################################################
|
||||||
|
|
@ -510,7 +525,6 @@ class CalendarEvent(models.Model):
|
||||||
)
|
)
|
||||||
orphaned_events = orphaned_events._to_sync()
|
orphaned_events = orphaned_events._to_sync()
|
||||||
if orphaned_events:
|
if orphaned_events:
|
||||||
_logger.info(f"Deleting orphaned events {orphaned_events.ids}")
|
|
||||||
orphaned_events.with_context(caldav_no_sync=True).with_user(user).unlink()
|
orphaned_events.with_context(caldav_no_sync=True).with_user(user).unlink()
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
|
|
@ -550,7 +564,6 @@ class CalendarEvent(models.Model):
|
||||||
if not recurrency_vals.get("follow_recurrence"):
|
if not recurrency_vals.get("follow_recurrence"):
|
||||||
recurrency_vals = {}
|
recurrency_vals = {}
|
||||||
|
|
||||||
_logger.info(f"Creating with vals: {values | recurrency_vals}")
|
|
||||||
new_event = self.with_context(caldav_no_sync=True).create(
|
new_event = self.with_context(caldav_no_sync=True).create(
|
||||||
values | recurrency_vals
|
values | recurrency_vals
|
||||||
)
|
)
|
||||||
|
|
@ -561,20 +574,12 @@ class CalendarEvent(models.Model):
|
||||||
synced_events |= new_event
|
synced_events |= new_event
|
||||||
continue
|
continue
|
||||||
elif outdated or not owned:
|
elif outdated or not owned:
|
||||||
_logger.info(
|
pass
|
||||||
f"Event {existing_instance.caldav_uid}-{existing_instance.caldav_recurrence_id} "
|
|
||||||
f"{'outdated' if outdated else ''}"
|
|
||||||
f"{'not owned by user' + user.name if not owned else ''}."
|
|
||||||
f" Skipping."
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
changed_vals = existing_instance._get_recurrence_changes(
|
changed_vals = existing_instance._get_recurrence_changes(
|
||||||
recurrency_vals
|
recurrency_vals
|
||||||
) | existing_instance._get_value_changes(values)
|
) | existing_instance._get_value_changes(values)
|
||||||
if changed_vals:
|
if changed_vals:
|
||||||
_logger.info(
|
|
||||||
f"Updating event {existing_instance.caldav_uid}-{existing_instance.caldav_recurrence_id} with : {changed_vals}"
|
|
||||||
)
|
|
||||||
existing_instance.with_context(
|
existing_instance.with_context(
|
||||||
caldav_no_sync=True,
|
caldav_no_sync=True,
|
||||||
).write(changed_vals)
|
).write(changed_vals)
|
||||||
|
|
@ -660,10 +665,11 @@ class CalendarEvent(models.Model):
|
||||||
|
|
||||||
if not rrule:
|
if not rrule:
|
||||||
return {}
|
return {}
|
||||||
|
if rrule.get("until"):
|
||||||
|
rrule["until"] = rrule.get("until")[0].astimezone(utc)
|
||||||
rrule_str = rrule.to_ical() and rrule.to_ical().decode("utf-8")
|
rrule_str = rrule.to_ical() and rrule.to_ical().decode("utf-8")
|
||||||
rrule_params = self.env["calendar.recurrence"]._rrule_parse(
|
rrule_params = self.env["calendar.recurrence"]._rrule_parse(
|
||||||
"RRULE:" + rrule_str, component.decoded("dtstart")
|
"RRULE:" + rrule_str, component.get("dtstart").dt.astimezone(utc)
|
||||||
)
|
)
|
||||||
vals = {
|
vals = {
|
||||||
"recurrency": True,
|
"recurrency": True,
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,27 @@ _logger = logging.getLogger(__name__)
|
||||||
class RecurrenceRule(models.Model):
|
class RecurrenceRule(models.Model):
|
||||||
_inherit = "calendar.recurrence"
|
_inherit = "calendar.recurrence"
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _default_uid(self):
|
|
||||||
return uuid.uuid4()
|
|
||||||
|
|
||||||
caldav_uid = fields.Char(
|
caldav_uid = fields.Char(
|
||||||
default=_default_uid,
|
|
||||||
readonly=True,
|
readonly=True,
|
||||||
|
copy=False,
|
||||||
)
|
)
|
||||||
|
_sql_constraints = [
|
||||||
|
("caldav_uid_unique", "UNIQUE (caldav_uid)", "caldav_uid must be unique")
|
||||||
|
]
|
||||||
|
|
||||||
def _stop_at(self, event):
|
@api.model_create_multi
|
||||||
detached_events = super()._stop_at(event)
|
def create(self, vals_list):
|
||||||
detached_events._recompute_caldav_uid()
|
for vals in vals_list:
|
||||||
detached_events._recompute_caldav_recurrence_id()
|
vals["caldav_uid"] = str(uuid.uuid4())
|
||||||
self.calendar_event_ids._sync_recurrence_to_caldav()
|
return super().create(vals_list)
|
||||||
# detached_events._sync_recurrence_to_caldav()
|
|
||||||
|
@api.model
|
||||||
|
def _detach_events(self, events):
|
||||||
|
"""When events are detached from a recurrence, their CalDAV UID and recurrence-id
|
||||||
|
are no longer going to be valid, so we remove them from the server. They may then
|
||||||
|
be re-written to the server with their new IDs later, but we don't care about
|
||||||
|
that here."""
|
||||||
|
detached_events = super()._detach_events(events)
|
||||||
|
for event in detached_events:
|
||||||
|
event._sync_unlink_to_caldav()
|
||||||
return detached_events
|
return detached_events
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue