further work on recurring events. cleaned up the code for them quite a bit.

This commit is contained in:
Marc Durepos 2024-05-24 19:59:25 -04:00
parent 0c93437e2c
commit b3367a2704

View file

@ -52,12 +52,13 @@ class CalendarEvent(models.Model):
def write(self, vals): def write(self, vals):
res = super(CalendarEvent, self).write(vals) res = super(CalendarEvent, self).write(vals)
if not self.env.context.get('caldav_no_sync') and self.id: if not self.env.context.get('caldav_no_sync') and self.ids:
try: for rec in self:
_logger.debug(f"Updating event {self.name} in CalDAV") try:
self.sync_update_to_caldav() _logger.debug(f"Updating event {self.name} in CalDAV")
except Exception as e: rec.sync_update_to_caldav()
_logger.error(f"Failed to update event in CalDAV server: {e}") except Exception as e:
_logger.error(f"Failed to update event in CalDAV server: {e}")
return res return res
def unlink(self): def unlink(self):
@ -181,34 +182,77 @@ class CalendarEvent(models.Model):
for component in ical_event.subcomponents: for component in ical_event.subcomponents:
if isinstance(component, Event): if isinstance(component, Event):
uid = str(component.get('uid')) uid = str(component.get('uid'))
rrule = component.get('rrule') recurrence_id = str(component.get('recurrence-id'))
if rrule: if recurrence_id == 'None':
dtstart = component.decoded('dtstart') recurrence_id = ''
if dtstart.tzinfo is not None: caldav_uids.add(f"{uid}{recurrence_id}")
dtstart = dtstart.replace(tzinfo=None)
rrule_str = rrule.to_ical().decode('utf-8')
rrule = rrulestr(rrule_str, dtstart=dtstart)
instances = list(rrule.between(now, one_year_later))
for instance_start in instances:
recurrence_id = instance_start.strftime('%Y%m%dT%H%M%S')
instance_uid = f"{uid}-{recurrence_id}"
caldav_uids.add(instance_uid)
else:
caldav_uids.add(uid)
_logger.info(f"CalDAV UIDs fetched: {caldav_uids}") _logger.info(f"CalDAV UIDs fetched: {caldav_uids}")
# Remove Odoo events that no longer exist on the CalDAV server # Remove Odoo events that no longer exist on the CalDAV server
odoo_events = self.search([('caldav_uid', '!=', False)]) odoo_events = self.search([('caldav_uid', '!=', False)])
for event in odoo_events: for event in odoo_events:
if event.caldav_recurrence_id: recurrence_id = event.caldav_recurrence_id or ''
if event.caldav_recurrence_id not in caldav_uids: event_uid = f"{event.caldav_uid}{event.caldav_recurrence_id or ''}"
_logger.info(f"Deleting orphan event {event.name} with Recurrence ID {event.caldav_recurrence_id}") if event_uid not in caldav_uids:
event.with_context(caldav_no_sync=True).unlink() _logger.info(f"Deleting orphan event {event.name} with UID {event.caldav_uid} "
elif event.caldav_uid not in caldav_uids: f"and Recurrence ID {event.caldav_recurrence_id or ''}")
_logger.info(f"Deleting orphan event {event.name} with UID {event.caldav_uid}")
event.with_context(caldav_no_sync=True).unlink() event.with_context(caldav_no_sync=True).unlink()
@api.model
def _get_existing_instance(self, uid, recurrence_id):
instance = self.env['calendar.event'].search([
('caldav_uid', '=', uid),
('recurrence_id', '=', recurrence_id)
])
return instance or self.env['calendar.event'].search([
('caldav_uid', '=', 'uid'),
('recurrence_id', '=', False),
])
def _get_recurrency_values_from_ical_event(self, component):
""" Match the fields from calendar.event (recurring fields) to the fields specified in RRULE at
https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html """
rrule = component.get('rrule')
if not rrule:
if not self.recurrency:
# No change, this was already not a recurring event
return {}
else:
# This was a recurring event and has been made non-recurring
if self.recurrence_id.base_event_id != self:
# This is not the base event, so change its recurrency only
return {'recurrence_update': 'self_only', 'recurrency': False, 'follow_recurrence': False}
else:
# This is the base event, so change all events in the list
return {'recurrence_update': 'all_events', 'recurrency': False}
rrule_str = rrule.to_ical().decode('utf-8')
sequence = component.get('sequence')
if sequence and sequence != 0:
# This is not the base event so we can't change recurrence properties
return {}
caldav_recurrence_id = component.get('recurrence-id')
rrule_params = self.env['calendar.recurrence']._rrule_parse(rrule_str, component.decoded('dtstart'))
vals = {
'recurrency': True,
'follow_recurrence': True,
'caldav_recurrence_id': caldav_recurrence_id,
'recurrence_update': 'all_events',
'rrule_type': rrule_params.get('rrule_type'),
'end_type': rrule_params.get('end_type'),
'interval': rrule_params.get('interval'),
'count': rrule_params.get('count'),
'month_by': rrule_params.get('monty_by'),
'day': rrule_params.get('day'),
'byday': rrule_params.get('byday'),
'until': rrule_params.get('until'),
}
if rrule_params.get('weekday'):
vals.update(rrule_params.get('weekday'))
return vals
def sync_event_from_ical(self, ical_event): def sync_event_from_ical(self, ical_event):
email_regex = re.compile(r'[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+') email_regex = re.compile(r'[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+')
now = datetime.now() now = datetime.now()
@ -217,8 +261,9 @@ class CalendarEvent(models.Model):
for component in ical_event.subcomponents: for component in ical_event.subcomponents:
if isinstance(component, Event): if isinstance(component, Event):
uid = str(component.get('uid')) uid = component.get('uid')
rrule = component.get('rrule') recurrence_id = component.get(
'recurrence_id') # Unique identifier for a single event in a recurrence set
attendees = component.get('attendee', []) attendees = component.get('attendee', [])
if isinstance(attendees, vCalAddress): if isinstance(attendees, vCalAddress):
@ -239,83 +284,37 @@ class CalendarEvent(models.Model):
attendee_ids = self.env['res.partner'].search([('email', 'in', attendees_emails)]) attendee_ids = self.env['res.partner'].search([('email', 'in', attendees_emails)])
if rrule: existing_instance = self._get_existing_instance(uid, recurrence_id)
dtstart = component.decoded('dtstart') start = component.decoded('dtstart')
dtend = component.decoded('dtend') if isinstance(start, datetime):
# Convert to naive datetime start = start.replace(tzinfo=None)
if dtstart.tzinfo: end = component.decoded('dtend')
dtstart = dtstart.replace(tzinfo=None) if isinstance(end, datetime):
if dtend.tzinfo: end = end.replace(tzinfo=None)
dtend = dtend.replace(tzinfo=None) values = {
rrule_str = rrule.to_ical().decode('utf-8') 'name': str(component.get('summary')),
rrule = rrulestr(rrule_str, dtstart=dtstart) 'start': start,
instances = list(rrule.between(now, one_year_later)) 'stop': end,
'description': str(component.get('description')),
for instance_start in instances: 'location': str(component.get('location')),
instance_stop = instance_start + (dtend - dtstart) 'caldav_uid': uid,
recurrence_id = instance_start.strftime('%Y%m%dT%H%M%S') 'partner_ids': [(6, 0, attendee_ids.ids)],
instance_uid = f"{uid}-{recurrence_id}" }
existing_instance = self.search([('caldav_recurrence_id', '=', instance_uid)], limit=1) recurrency_vals = self._get_recurrency_values_from_ical_event(component)
if recurrency_vals:
_logger.info(f"Processing instance UID: {instance_uid}") values.update(recurrency_vals)
_logger.info(f"Instance start: {instance_start}, stop: {instance_stop}") if not existing_instance:
_logger.info(f"Creating with vals: {values}")
if not existing_instance: self.with_context({'caldav_no_sync': True}).create(values)
_logger.info(f"Creating new recurring event instance with UID {instance_uid}")
self.with_context({'caldav_no_sync': True}).create({
'name': str(component.get('summary')),
'start': instance_start,
'stop': instance_stop,
'description': str(component.get('description')),
'location': str(component.get('location')),
'caldav_uid': uid,
'caldav_recurrence_id': instance_uid,
'partner_ids': [(6, 0, attendee_ids.ids)],
})
else:
_logger.info(f"Updating existing recurring event instance with UID {instance_uid}")
existing_instance.with_context({'caldav_no_sync': True}).write({
'name': str(component.get('summary')),
'start': instance_start,
'stop': instance_stop,
'description': str(component.get('description')),
'location': str(component.get('location')),
'partner_ids': [(6, 0, attendee_ids.ids)],
})
else: else:
start = component.decoded('dtstart') _logger.info(f"Updating with vals: {values}")
stop = component.decoded('dtend') if recurrency_vals and recurrency_vals.get('recurrency'):
if isinstance(start, datetime): existing_instance.write({
start = start.replace(tzinfo=None) 'recurrency': True,
if isinstance(stop, datetime): 'follow_recurrence': True,
stop = stop.replace(tzinfo=None)
event = self.search([('caldav_uid', '=', uid)], limit=1)
if not event:
_logger.info(f"Creating new event {str(component.get('summary'))} with UID {uid}")
self.with_context({'caldav_no_sync': True}).create({
'name': str(component.get('summary')),
'start': start,
'stop': stop,
'description': str(component.get('description')),
'location': str(component.get('location')),
'caldav_uid': uid,
'partner_ids': [(6, 0, attendee_ids.ids)],
}) })
else: existing_instance.with_context({'caldav_no_sync': True}).write(values)
_logger.info(f"Updating existing event {event.name} with UID {uid}")
existing_partner_ids = set(event.partner_ids.ids)
new_partner_ids = set(attendee_ids.ids)
combined_partner_ids = list(existing_partner_ids.union(new_partner_ids))
event.with_context({'caldav_no_sync': True}).write({
'name': str(component.get('summary')),
'start': start,
'stop': stop,
'description': str(component.get('description')),
'location': str(component.get('location')),
'partner_ids': [(6, 0, combined_partner_ids)],
})
@staticmethod @staticmethod
def _html_to_text(html): def _html_to_text(html):