From b3367a2704e69ef0d18b73e599b18f7db859ac70 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Fri, 24 May 2024 19:59:25 -0400 Subject: [PATCH] further work on recurring events. cleaned up the code for them quite a bit. --- caldav_sync/models/calendar_event.py | 203 +++++++++++++-------------- 1 file changed, 101 insertions(+), 102 deletions(-) diff --git a/caldav_sync/models/calendar_event.py b/caldav_sync/models/calendar_event.py index 3722f83..a7811b8 100644 --- a/caldav_sync/models/calendar_event.py +++ b/caldav_sync/models/calendar_event.py @@ -52,12 +52,13 @@ class CalendarEvent(models.Model): def write(self, vals): res = super(CalendarEvent, self).write(vals) - if not self.env.context.get('caldav_no_sync') and self.id: - try: - _logger.debug(f"Updating event {self.name} in CalDAV") - self.sync_update_to_caldav() - except Exception as e: - _logger.error(f"Failed to update event in CalDAV server: {e}") + if not self.env.context.get('caldav_no_sync') and self.ids: + for rec in self: + try: + _logger.debug(f"Updating event {self.name} in CalDAV") + rec.sync_update_to_caldav() + except Exception as e: + _logger.error(f"Failed to update event in CalDAV server: {e}") return res def unlink(self): @@ -181,34 +182,77 @@ class CalendarEvent(models.Model): for component in ical_event.subcomponents: if isinstance(component, Event): uid = str(component.get('uid')) - rrule = component.get('rrule') - if rrule: - dtstart = component.decoded('dtstart') - if dtstart.tzinfo is not None: - 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) + recurrence_id = str(component.get('recurrence-id')) + if recurrence_id == 'None': + recurrence_id = '' + caldav_uids.add(f"{uid}{recurrence_id}") _logger.info(f"CalDAV UIDs fetched: {caldav_uids}") # Remove Odoo events that no longer exist on the CalDAV server odoo_events = self.search([('caldav_uid', '!=', False)]) for event in odoo_events: - if event.caldav_recurrence_id: - if event.caldav_recurrence_id not in caldav_uids: - _logger.info(f"Deleting orphan event {event.name} with Recurrence ID {event.caldav_recurrence_id}") - event.with_context(caldav_no_sync=True).unlink() - elif event.caldav_uid not in caldav_uids: - _logger.info(f"Deleting orphan event {event.name} with UID {event.caldav_uid}") + recurrence_id = event.caldav_recurrence_id or '' + event_uid = f"{event.caldav_uid}{event.caldav_recurrence_id or ''}" + if event_uid not in caldav_uids: + _logger.info(f"Deleting orphan event {event.name} with UID {event.caldav_uid} " + f"and Recurrence ID {event.caldav_recurrence_id or ''}") 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): email_regex = re.compile(r'[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+') now = datetime.now() @@ -217,8 +261,9 @@ class CalendarEvent(models.Model): for component in ical_event.subcomponents: if isinstance(component, Event): - uid = str(component.get('uid')) - rrule = component.get('rrule') + uid = component.get('uid') + recurrence_id = component.get( + 'recurrence_id') # Unique identifier for a single event in a recurrence set attendees = component.get('attendee', []) if isinstance(attendees, vCalAddress): @@ -239,83 +284,37 @@ class CalendarEvent(models.Model): attendee_ids = self.env['res.partner'].search([('email', 'in', attendees_emails)]) - if rrule: - dtstart = component.decoded('dtstart') - dtend = component.decoded('dtend') - # Convert to naive datetime - if dtstart.tzinfo: - dtstart = dtstart.replace(tzinfo=None) - if dtend.tzinfo: - dtend = dtend.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: - instance_stop = instance_start + (dtend - dtstart) - recurrence_id = instance_start.strftime('%Y%m%dT%H%M%S') - instance_uid = f"{uid}-{recurrence_id}" - existing_instance = self.search([('caldav_recurrence_id', '=', instance_uid)], limit=1) - - _logger.info(f"Processing instance UID: {instance_uid}") - _logger.info(f"Instance start: {instance_start}, stop: {instance_stop}") - - if not existing_instance: - _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)], - }) + existing_instance = self._get_existing_instance(uid, recurrence_id) + start = component.decoded('dtstart') + if isinstance(start, datetime): + start = start.replace(tzinfo=None) + end = component.decoded('dtend') + if isinstance(end, datetime): + end = end.replace(tzinfo=None) + values = { + 'name': str(component.get('summary')), + 'start': start, + 'stop': end, + 'description': str(component.get('description')), + 'location': str(component.get('location')), + 'caldav_uid': uid, + 'partner_ids': [(6, 0, attendee_ids.ids)], + } + recurrency_vals = self._get_recurrency_values_from_ical_event(component) + if recurrency_vals: + values.update(recurrency_vals) + if not existing_instance: + _logger.info(f"Creating with vals: {values}") + self.with_context({'caldav_no_sync': True}).create(values) else: - start = component.decoded('dtstart') - stop = component.decoded('dtend') - if isinstance(start, datetime): - start = start.replace(tzinfo=None) - if isinstance(stop, datetime): - 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)], + _logger.info(f"Updating with vals: {values}") + if recurrency_vals and recurrency_vals.get('recurrency'): + existing_instance.write({ + 'recurrency': True, + 'follow_recurrence': True, }) - else: - _logger.info(f"Updating existing event {event.name} with UID {uid}") + existing_instance.with_context({'caldav_no_sync': True}).write(values) - 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 def _html_to_text(html):