caldav_sync: better sync with calls to _rewrite_recurrence

This commit is contained in:
Marc Durepos 2024-11-08 11:31:14 -05:00
parent 5ca1d56e54
commit 90d05d75a2
2 changed files with 59 additions and 23 deletions

View file

@ -102,7 +102,8 @@ class CalendarEvent(models.Model):
time.second, time.second,
) )
else: else:
event.caldav_uid = uuid.uuid4() if not self._context.get("caldav_keep_ids"):
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")
@ -143,7 +144,7 @@ class CalendarEvent(models.Model):
) )
) )
@api.depends("recurrency", "recurrence_id.base_event_id") @api.depends("recurrency", "recurrence_id", "recurrence_id.base_event_id")
def _compute_is_base_event(self): def _compute_is_base_event(self):
for rec in self: for rec in self:
rec.is_base_event = ( rec.is_base_event = (
@ -177,11 +178,9 @@ class CalendarEvent(models.Model):
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
events = super(
CalendarEvent, self.with_context({"caldav_no_sync": True})
).create(vals_list)
ctx = {"caldav_no_sync": True} ctx = {"caldav_no_sync": True}
events.with_context(ctx)._recompute_caldav_ids() events = super(CalendarEvent, self.with_context(**ctx)).create(vals_list)
events.with_context(**ctx)._recompute_caldav_ids()
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
@ -202,9 +201,7 @@ class CalendarEvent(models.Model):
_logger.error(f"Failed to sync event to CalDAV server: {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(vals)
vals
)
# Events sometimes get archived in Odoo in the process of updating recurrence # Events sometimes get archived in Odoo in the process of updating recurrence
# In that case, we will delete them from the CalDAV server before recreating # In that case, we will delete them from the CalDAV server before recreating
if "active" in vals and not vals.get("active"): if "active" in vals and not vals.get("active"):
@ -229,7 +226,7 @@ class CalendarEvent(models.Model):
start = utc.localize(self.start).astimezone(tz) start = utc.localize(self.start).astimezone(tz)
# We have an event in the recurrence chain that differs from the base # We have an event in the recurrence chain that differs from the base
# We need to add an iCalendar VEVENT with recurrence-id or update it # We need to add an iCalendar VEVENT with recurrence-id or update it
if self.is_base_event and start == self._get_ical_recurrence_id(): if self.is_base_event:
# We have a normal base event, so we just update it or create it # We have a normal base event, so we just update it or create it
self._update_base_caldav_event( self._update_base_caldav_event(
calendar, base_event, ical_event_data calendar, base_event, ical_event_data
@ -274,6 +271,8 @@ class CalendarEvent(models.Model):
def _update_ical_event_values( def _update_ical_event_values(
self, ical_event: icalendar.cal.Event, event_data: dict self, ical_event: icalendar.cal.Event, event_data: dict
): ):
sequence = (seq := ical_event.get("sequence") or 0) + 1
event_data.update(sequence=sequence)
for key, value in event_data.items(): for key, value in event_data.items():
ical_event[key] = value ical_event[key] = value
@ -307,13 +306,14 @@ class CalendarEvent(models.Model):
return index return index
def _sync_unlink_to_caldav(self): def _sync_unlink_to_caldav(self):
delete_all = self._context.get("caldav_delete_all", False)
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:
caldav_event = calendar.event_by_uid(self.caldav_uid) caldav_event = calendar.event_by_uid(self.caldav_uid)
if self.recurrence_id and not self.is_base_event: if not delete_all and self.recurrence_id and not self.is_base_event:
index = self._get_subcomponent_index_for_recurrence( index = self._get_subcomponent_index_for_recurrence(
caldav_event caldav_event
) )
@ -325,7 +325,7 @@ class CalendarEvent(models.Model):
# they can be base events but have an old caldav_uid from when # 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 # they were recurring. That's why we make sure that the start
# of the event matches. # of the event matches.
if self._matches_caldav_start(caldav_event): if delete_all or self._matches_caldav_start(caldav_event):
caldav_event.delete() caldav_event.delete()
except caldav.error.NotFoundError: except caldav.error.NotFoundError:
# No worries - it just didn't exist so nothing to sync # No worries - it just didn't exist so nothing to sync
@ -350,10 +350,16 @@ class CalendarEvent(models.Model):
We do, however, need to get a new caldav_uid and caldav_recurrence_id for all We do, however, need to get a new caldav_uid and caldav_recurrence_id for all
the events in the new chain. the events in the new chain.
""" """
ctx = {"keep_caldav_ids": False}
old_base = self.recurrence_id.base_event_id old_base = self.recurrence_id.base_event_id
super()._update_future_events(values, time_values, recurrence_values) super().with_context(**ctx)._update_future_events(
values, time_values, recurrence_values
)
events_to_refresh = self.recurrence_id._get_events_from(self.start) events_to_refresh = self.recurrence_id._get_events_from(self.start)
events_to_refresh._recompute_caldav_ids() # Future events are archived but not this one. We need to remove the old version
# from the calendar if it existed.
self._sync_unlink_to_caldav()
events_to_refresh.with_context(**ctx)._recompute_caldav_ids()
self._sync_write_to_caldav() self._sync_write_to_caldav()
old_base._sync_write_to_caldav() old_base._sync_write_to_caldav()
@ -369,6 +375,24 @@ class CalendarEvent(models.Model):
recurrence.base_event_id._sync_write_to_caldav() recurrence.base_event_id._sync_write_to_caldav()
return detached_events - self return detached_events - self
def _rewrite_recurrence(self, values, time_values, recurrence_values):
"""
When _rewrite_recurrence is called, it archives all the events in the
recurrence but only after messing with the recurrence_id. This breaks our check
on is_base_event, so we preemptively sync the deletion here.
Because this method is usually called with caldav_no_sync=True in the context
to avoid looping, we manually call _sync_write_to_caldav once the operation is
completed.
"""
for event in self.recurrence_id.calendar_event_ids:
event.with_context(caldav_delete_all=True)._sync_unlink_to_caldav()
res = super()._rewrite_recurrence(values, time_values, recurrence_values)
for event in self.recurrence_id.calendar_event_ids:
event._recompute_caldav_ids()
self._sync_write_to_caldav()
return res
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
@ -406,7 +430,13 @@ class CalendarEvent(models.Model):
return event_data return event_data
def _add_event_header_info(self, event_data: Dict) -> None: def _add_event_header_info(self, event_data: Dict) -> None:
"""Add event header info to a dict containing event data.""" """Add event header info to a dict containing event data.
IMPORTANT: the VEVENT sequence is set to 0. It should be changed if this method
is used to update an existing event!
:param event_data: The dictionary of event data to be updated with header info.
"""
event_data["uid"] = vText(self.caldav_uid) event_data["uid"] = vText(self.caldav_uid)
if self.name: if self.name:
event_data["summary"] = vText(self.name) event_data["summary"] = vText(self.name)
@ -417,6 +447,7 @@ class CalendarEvent(models.Model):
event_data["location"] = vText(self.location) event_data["location"] = vText(self.location)
if self.videocall_location: if self.videocall_location:
event_data["conference"] = self.videocall_location event_data["conference"] = self.videocall_location
event_data["sequence"] = 0
def _add_event_dates(self, event_data: Dict) -> None: def _add_event_dates(self, event_data: Dict) -> None:
"""Add pertinent dates to event data, based on self.""" """Add pertinent dates to event data, based on self."""
@ -564,9 +595,9 @@ class CalendarEvent(models.Model):
if not recurrency_vals.get("follow_recurrence"): if not recurrency_vals.get("follow_recurrence"):
recurrency_vals = {} recurrency_vals = {}
new_event = self.with_context(caldav_no_sync=True).create( new_event = self.with_context(
values | recurrency_vals caldav_no_sync=True, caldav_keep_ids=True
) ).create(values | recurrency_vals)
self.env["calendar.event"].flush_model() self.env["calendar.event"].flush_model()
if new_event.recurrency: if new_event.recurrency:
synced_events |= new_event.recurrence_id.calendar_event_ids synced_events |= new_event.recurrence_id.calendar_event_ids
@ -582,6 +613,7 @@ class CalendarEvent(models.Model):
if changed_vals: if changed_vals:
existing_instance.with_context( existing_instance.with_context(
caldav_no_sync=True, caldav_no_sync=True,
caldav_keep_ids=True,
).write(changed_vals) ).write(changed_vals)
if existing_instance.recurrency and existing_instance.is_base_event: if existing_instance.recurrency and existing_instance.is_base_event:
@ -773,14 +805,13 @@ class CalendarEvent(models.Model):
:return: True if the iCalendar component is outdated, False otherwise. :return: True if the iCalendar component is outdated, False otherwise.
""" """
outdated = False outdated = False
last_modified = component.get("last-modified") last_modified = component.get("dtstamp") and component.get("dtstamp").dt
if ( if (
existing_instance existing_instance
and last_modified and last_modified
and existing_instance not in synced_events and existing_instance not in synced_events
): ):
last_modified = last_modified.dt.astimezone(utc).replace(tzinfo=None) if last_modified < utc.localize(existing_instance.write_date):
if last_modified < existing_instance.write_date:
outdated = True outdated = True
return outdated return outdated

View file

@ -18,8 +18,13 @@ class RecurrenceRule(models.Model):
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
for vals in vals_list: if not self._context.get("caldav_keep_ids"):
vals["caldav_uid"] = str(uuid.uuid4()) for vals in vals_list:
vals["caldav_uid"] = str(uuid.uuid4())
else:
for vals in vals_list:
base_event = self.env["calendar.event"].browse(vals["base_event_id"])
vals.update(caldav_uid=base_event.caldav_uid)
return super().create(vals_list) return super().create(vals_list)
@api.model @api.model