caldav_sync: updating recurrences in the middle with future works, but it breaks singled out events before it (brings them back into the rrule chain)

This commit is contained in:
Marc Durepos 2024-11-07 10:33:26 -05:00
parent 421d609abd
commit ce7b27cc65
2 changed files with 63 additions and 21 deletions

View file

@ -64,16 +64,17 @@ def _extract_vcal_email(vcal_address):
class CalendarEvent(models.Model): class CalendarEvent(models.Model):
_inherit = "calendar.event" _inherit = "calendar.event"
caldav_uid = fields.Char( # CalDAV UID is either unique per event or the same for all events in a recurring
string="CalDAV UID", compute="_compute_caldav_uid", store=True # sequence. This field and caldav_recurrence_id are not calculated fields since
) # the recalculation timing is hard to control and we want to make sure we sync
# some modifications before the UID changes with recurrence_id and other changes on
# the record.
caldav_uid = fields.Char(string="CalDAV UID", store=True)
# Recurrence ID in iCalendar is the date or datetime the event would have # Recurrence ID in iCalendar is the date or datetime the event would have
# been at if it followed the sequence. It is set by calendar.recurrence # been at if it followed the sequence. It is set by calendar.recurrence
# when applying a recurrence. # when applying a recurrence.
caldav_recurrence_id = fields.Datetime( caldav_recurrence_id = fields.Datetime(
string="CalDAV Recurrence ID", string="CalDAV Recurrence ID",
compute="_compute_caldav_recurrence_id",
store=True,
) )
caldav_user_ids = fields.Many2many( caldav_user_ids = fields.Many2many(
comodel_name="res.users", comodel_name="res.users",
@ -86,17 +87,21 @@ class CalendarEvent(models.Model):
#### Field Computation Methods #### #### Field Computation Methods ####
################################### ###################################
@api.depends("recurrence_id") def _recompute_caldav_uid(self):
def _compute_caldav_uid(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: else:
event.caldav_uid = uuid.uuid4() event.caldav_uid = uuid.uuid4()
_logger.info(f"New CalDAV UID: {event.caldav_uid}")
@api.depends("recurrence_id.dtstart") @api.depends("recurrence_id", "recurrence_id.dtstart")
def _compute_caldav_recurrence_id(self): def _recompute_caldav_recurrence_id(self):
for event in self: for event in self:
_logger.info(
f"Recomputing caldav_recurrence_id for event {event} at {event.start}"
)
if event.recurrence_id: if event.recurrence_id:
time = event.recurrence_id.dtstart.time() time = event.recurrence_id.dtstart.time()
date = event.start.date() date = event.start.date()
@ -186,6 +191,9 @@ class CalendarEvent(models.Model):
events = super( events = super(
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}
events.with_context(ctx)._recompute_caldav_uid()
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
@ -279,17 +287,29 @@ 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)
try: try:
_logger.info(f"Removing CalDAV event {self.caldav_uid}") _logger.info(
ical_event = self._find_in_caldav_calendar(calendar) f"Removing CalDAV event {self.caldav_uid}, Odoo ID {self.id} starting at {self.start}"
if ical_event: )
ical_event.delete() caldav_event = self._find_in_caldav_calendar(
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()
except caldav.error.NotFoundError: except caldav.error.NotFoundError:
_logger.info( _logger.info(
f"CalDAV event {self.caldav_uid} not found on server during deletion." f"CalDAV event {self.caldav_uid} starting at {self.start}"
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):
for event in self._to_sync():
event._sync_write_to_caldav()
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
@ -340,7 +360,8 @@ class CalendarEvent(models.Model):
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."""
event_tz = timezone(self.event_tz) tz = self.event_tz or self.env.user.tz
event_tz = timezone(tz)
event_data["last-modified"] = vDatetime( event_data["last-modified"] = vDatetime(
utc.localize(self.write_date).astimezone(event_tz) utc.localize(self.write_date).astimezone(event_tz)
) )
@ -408,7 +429,10 @@ class CalendarEvent(models.Model):
return mapping.get(state, "NEEDS-ACTION") return mapping.get(state, "NEEDS-ACTION")
def _find_in_caldav_calendar( def _find_in_caldav_calendar(
self, calendar: caldav.Calendar, force_recurrence_id: bool = False self,
calendar: caldav.Calendar,
force_recurrence_id: bool = False,
match_start: bool = False,
) -> Optional[caldav.Event]: ) -> Optional[caldav.Event]:
"""Find this event in a given CalDAV Calendar. If the event is """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 recurring but is not a base event, it will be matched with its
@ -425,16 +449,26 @@ class CalendarEvent(models.Model):
match_recurrence_id = ( match_recurrence_id = (
force_recurrence_id or self.recurrence_id and not self.is_base_event force_recurrence_id or self.recurrence_id and not self.is_base_event
) )
if not match_recurrence_id: if not match_recurrence_id and not match_start:
return calendar.event_by_uid(self.caldav_uid) calendar.event_by_uid(self.caldav_uid)
else: else:
events = calendar.events() events = calendar.events()
for event in events: for event in events:
event_instance = event.icalendar_instance event_instance = event.icalendar_component
if ( if (
event_instance.get("uid") == self.caldav_uid event_instance.get("uid") == self.caldav_uid
and event_instance.get("recurrence-id").dt and (
== self._get_ical_recurrence_id() 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 event
return None return None

View file

@ -16,3 +16,11 @@ class RecurrenceRule(models.Model):
default=_default_uid, default=_default_uid,
readonly=True, readonly=True,
) )
def _stop_at(self, event):
detached_events = super()._stop_at(event)
detached_events._recompute_caldav_uid()
detached_events._recompute_caldav_recurrence_id()
self.calendar_event_ids._sync_recurrence_to_caldav()
# detached_events._sync_recurrence_to_caldav()
return detached_events