From 957de3221cd9f84d82c00d246be88254b10626bc Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Thu, 23 May 2024 16:03:55 -0400 Subject: [PATCH] testing via the interface seems to be working, though some parameters are not set to sync yet. --- caldav_sync/models/__init__.py | 1 - caldav_sync/models/caldav_calendar.py | 9 -- caldav_sync/models/calendar_event.py | 134 +++++++++++++++++++------- 3 files changed, 100 insertions(+), 44 deletions(-) delete mode 100644 caldav_sync/models/caldav_calendar.py diff --git a/caldav_sync/models/__init__.py b/caldav_sync/models/__init__.py index 8ab1c11..1ac5a2d 100644 --- a/caldav_sync/models/__init__.py +++ b/caldav_sync/models/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- from . import calendar_event from . import res_users -from . import caldav_calendar diff --git a/caldav_sync/models/caldav_calendar.py b/caldav_sync/models/caldav_calendar.py deleted file mode 100644 index e820c5e..0000000 --- a/caldav_sync/models/caldav_calendar.py +++ /dev/null @@ -1,9 +0,0 @@ -from odoo import models, fields - -class CaldavCalendar(models.Model): - _name = 'caldav.calendar' - _description = 'CalDAV Calendar' - - user_id = fields.Many2one('res.users', string='User', required=True, ondelete='cascade') - name = fields.Char('Name', required=True) - url = fields.Char('URL', required=True) diff --git a/caldav_sync/models/calendar_event.py b/caldav_sync/models/calendar_event.py index 4d0fbe3..0ffb955 100644 --- a/caldav_sync/models/calendar_event.py +++ b/caldav_sync/models/calendar_event.py @@ -1,7 +1,9 @@ +import uuid from odoo import models, api, fields import caldav -from caldav.elements import dav, cdav import logging +from datetime import datetime +from icalendar import Calendar, Event _logger = logging.getLogger(__name__) @@ -12,18 +14,35 @@ class CalendarEvent(models.Model): @api.model def create(self, vals): + if not vals.get('caldav_uid'): + vals['caldav_uid'] = str(uuid.uuid4()) event = super(CalendarEvent, self).create(vals) - event.sync_to_caldav() + if not self.env.context.get('caldav_no_sync'): + try: + _logger.debug(f"Creating event {event.name} in CalDAV") + event.sync_create_to_caldav() + except Exception as e: + _logger.error(f"Failed to create event in CalDAV server: {e}") return event def write(self, vals): res = super(CalendarEvent, self).write(vals) - self.sync_to_caldav() + if not self.env.context.get('caldav_no_sync'): + try: + _logger.debug(f"Updating event {self.name} in CalDAV") + self.with_context(caldav_no_sync=True).sync_update_to_caldav() + except Exception as e: + _logger.error(f"Failed to update event in CalDAV server: {e}") return res def unlink(self): - for event in self: - event.remove_from_caldav() + if not self.env.context.get('caldav_no_sync'): + for event in self: + try: + _logger.debug(f"Removing event {event.name} from CalDAV") + event.with_context(caldav_no_sync=True).sync_remove_from_caldav() + except Exception as e: + _logger.error(f"Failed to delete event from CalDAV server: {e}") return super(CalendarEvent, self).unlink() def _is_caldav_enabled(self): @@ -38,45 +57,68 @@ class CalendarEvent(models.Model): password=user.caldav_password ) - def sync_to_caldav(self): + def sync_create_to_caldav(self): if not self._is_caldav_enabled(): return client = self._get_caldav_client() - calendar = client.calendar(self.env.user.caldav_calendar_url) + calendar = client.calendar(url=self.env.user.caldav_calendar_url) for event in self: ical_event = event._get_icalendar() - if event.caldav_uid: - caldav_event = calendar.event_by_uid(event.caldav_uid) - caldav_event.save(ical_event) - else: + try: + _logger.debug(f"Creating new CalDAV event for {event.name}") caldav_event = calendar.add_event(ical_event) - event.caldav_uid = caldav_event.id + caldav_uid = caldav_event.vobject_instance.vevent.uid.value + _logger.debug(f"New 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}") - def remove_from_caldav(self): + def sync_update_to_caldav(self): if not self._is_caldav_enabled(): return client = self._get_caldav_client() - calendar = client.calendar(self.env.user.caldav_calendar_url) + calendar = client.calendar(url=self.env.user.caldav_calendar_url) + for event in self: + ical_event = event._get_icalendar() + try: + _logger.debug(f"Updating existing CalDAV event {event.caldav_uid}") + calendar.save_event(ical=ical_event) + except Exception as e: + _logger.error(f"Failed to sync event to CalDAV server: {e}") + + def sync_remove_from_caldav(self): + if not self._is_caldav_enabled(): + return + client = self._get_caldav_client() + calendar = client.calendar(url=self.env.user.caldav_calendar_url) for event in self: if event.caldav_uid: - caldav_event = calendar.event_by_uid(event.caldav_uid) - caldav_event.delete() + try: + _logger.debug(f"Removing CalDAV event {event.caldav_uid}") + caldav_event = calendar.event_by_uid(event.caldav_uid) + caldav_event.delete() + except caldav.error.NotFoundError: + _logger.warning(f"CalDAV event {event.caldav_uid} not found on server.") + except Exception as e: + _logger.error(f"Failed to remove event from CalDAV server: {e}") def _get_icalendar(self): - from icalendar import Calendar, Event calendar = Calendar() calendar.add('prodid', '-//Odoo//mxm.dk//') calendar.add('version', '2.0') for event in self: ical_event = Event() - ical_event.add('uid', event.caldav_uid or '') - ical_event.add('dtstamp', event.write_date) - ical_event.add('dtstart', event.start) - ical_event.add('dtend', event.stop) - ical_event.add('summary', event.name) - ical_event.add('description', event.description) - ical_event.add('location', event.location) + ical_event.add('uid', event.caldav_uid) + ical_event.add('dtstamp', event.write_date.replace(tzinfo=None)) + ical_event.add('dtstart', event.start.replace(tzinfo=None)) + ical_event.add('dtend', event.stop.replace(tzinfo=None)) + if event.name: + ical_event.add('summary', event.name) + if event.description: + ical_event.add('description', event.description) + if event.location: + ical_event.add('location', event.location) calendar.add_component(ical_event) return calendar.to_ical() @@ -94,32 +136,56 @@ class CalendarEvent(models.Model): if not self._is_caldav_enabled(): return client = self._get_caldav_client() - calendar = client.calendar(self.env.user.caldav_calendar_url) + calendar = client.calendar(url=self.env.user.caldav_calendar_url) events = calendar.events() + caldav_uids = set() + + _logger.info(f"Polling CalDAV server for user {self.env.user.name}") + for caldav_event in events: - ical_event = caldav_event.icalendar_instance - self.sync_event_from_ical(ical_event) + ical_event = caldav_event.icalendar_instance + self.sync_event_from_ical(ical_event) + for component in ical_event.subcomponents: + if isinstance(component, Event): + uid = str(component.get('uid')) + caldav_uids.add(uid) + + _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_uid not in caldav_uids: + _logger.info(f"Deleting orphan event {event.name} with UID {event.caldav_uid}") + event.with_context(caldav_no_sync=True).unlink() def sync_event_from_ical(self, ical_event): - from icalendar import Event for component in ical_event.subcomponents: if isinstance(component, Event): uid = str(component.get('uid')) event = self.search([('caldav_uid', '=', uid)], limit=1) + 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) if not event: - self.create({ + _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': component.decoded('dtstart'), - 'stop': component.decoded('dtend'), + 'start': start, + 'stop': stop, 'description': str(component.get('description')), 'location': str(component.get('location')), 'caldav_uid': uid, }) else: - event.write({ + _logger.info(f"Updating existing event {event.name} with UID {uid}") + event.with_context({'caldav_no_sync': True}).write({ 'name': str(component.get('summary')), - 'start': component.decoded('dtstart'), - 'stop': component.decoded('dtend'), + 'start': start, + 'stop': stop, 'description': str(component.get('description')), 'location': str(component.get('location')), })