From 9d2ea9a8fa18a11373e9d5becdeb9d96ed7ae62a Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Thu, 23 May 2024 13:11:00 -0400 Subject: [PATCH] caldav_sync: removed the ability to choose a calendar, use calendar url instead --- caldav_sync/README.rst | 7 +- caldav_sync/__manifest__.py | 2 +- caldav_sync/models/calendar_event.py | 230 +++++++++++--------------- caldav_sync/models/res_users.py | 37 +---- caldav_sync/tests/test_caldav_sync.py | 11 +- caldav_sync/views/res_users_views.xml | 10 +- 6 files changed, 111 insertions(+), 186 deletions(-) diff --git a/caldav_sync/README.rst b/caldav_sync/README.rst index 0e77a37..c5ae537 100644 --- a/caldav_sync/README.rst +++ b/caldav_sync/README.rst @@ -1,3 +1,4 @@ + CalDAV Synchronization ====================== @@ -20,20 +21,18 @@ Features - Synchronize Odoo calendar events with CalDAV servers. - Create, update, and delete events in Odoo and reflect changes on the CalDAV server. - Poll CalDAV server for changes and update Odoo calendar accordingly. -- Allow users to select which calendar to synchronize with on the CalDAV server. Configuration ------------- 1. Install the module in Odoo. 2. Go to the User settings in Odoo. -3. Enter the CalDAV server URL, username, and password. -4. Fetch the available calendars from the CalDAV server and select the one to synchronize with. +3. Enter the CalDAV calendar URL, username, and password. Usage ----- -1. Create a calendar event in Odoo and it will be synchronized with the selected CalDAV calendar. +1. Create a calendar event in Odoo and it will be synchronized with the CalDAV calendar. 2. Update the event in Odoo and the changes will reflect on the CalDAV server. 3. Delete the event in Odoo and it will be removed from the CalDAV server. 4. Changes made to the calendar on the CalDAV server will be polled and updated in Odoo. diff --git a/caldav_sync/__manifest__.py b/caldav_sync/__manifest__.py index 5e84d78..d3a5d54 100644 --- a/caldav_sync/__manifest__.py +++ b/caldav_sync/__manifest__.py @@ -8,7 +8,7 @@ { 'name': 'CalDAV Synchronization', - 'version': '17.0.0.2.0', + 'version': '17.0.0.3.0', 'license': 'LGPL-3', 'category': 'Productivity', 'summary': 'Synchronize Odoo Calendar Events with CalDAV Servers', diff --git a/caldav_sync/models/calendar_event.py b/caldav_sync/models/calendar_event.py index 207fe2e..4d0fbe3 100644 --- a/caldav_sync/models/calendar_event.py +++ b/caldav_sync/models/calendar_event.py @@ -1,159 +1,125 @@ - -from odoo import models, fields, api +from odoo import models, api, fields import caldav +from caldav.elements import dav, cdav import logging -from icalendar import Calendar, Event, vCalAddress, vText _logger = logging.getLogger(__name__) class CalendarEvent(models.Model): _inherit = 'calendar.event' - caldav_uid = fields.Char('CalDAV UID') + caldav_uid = fields.Char(string='CalDAV UID', readonly=True) @api.model - def create(self, values): - event = super(CalendarEvent, self).create(values) - if event._is_caldav_enabled() and not self.env.context.get('skip_caldav_sync'): - try: - event.sync_to_caldav() - except Exception as e: - _logger.error(f"Failed to sync event to CalDAV: {e}") + def create(self, vals): + event = super(CalendarEvent, self).create(vals) + event.sync_to_caldav() return event - def write(self, values): - result = super(CalendarEvent, self).write(values) - if self._is_caldav_enabled() and not self.env.context.get('skip_caldav_sync'): - try: - self.sync_to_caldav() - except Exception as e: - _logger.error(f"Failed to sync event to CalDAV: {e}") - return result + def write(self, vals): + res = super(CalendarEvent, self).write(vals) + self.sync_to_caldav() + return res def unlink(self): - if self._is_caldav_enabled(): - try: - self.remove_from_caldav() - except Exception as e: - _logger.error(f"Failed to remove event from CalDAV: {e}") + for event in self: + event.remove_from_caldav() return super(CalendarEvent, self).unlink() - def sync_to_caldav(self): - for event in self: - if event._is_caldav_enabled(): - client = event._get_caldav_client() - calendar = client.calendar(url=event._get_caldav_calendar_url()) - caldav_event = calendar.add_event(event._get_icalendar()) - if caldav_event.id: - event.with_context(skip_caldav_sync=True).write({'caldav_uid': caldav_event.id}) - else: - _logger.error(f"Failed to sync event to CalDAV: Event ID not returned") - - def remove_from_caldav(self): - for event in self: - if event.caldav_uid and event._is_caldav_enabled(): - client = event._get_caldav_client() - calendar = client.calendar(url=event._get_caldav_calendar_url()) - caldav_event = calendar.event_by_uid(event.caldav_uid) - if caldav_event: - caldav_event.delete() - - @api.model - def poll_caldav_server(self): - """Poll the CalDAV server and synchronize events for all users""" - _logger.info('Polling CalDAV server for updates...') - users = self.env['res.users'].search([]) - for user in users: - if user._is_caldav_enabled(): - try: - _logger.info(f'Polling CalDAV server for user {user.name}...') - client = user._get_caldav_client() - calendar = client.calendar(url=user.caldav_calendar_id.url) - events = calendar.events() - - # Collect all current CalDAV UIDs for this user - current_uids = set(event.caldav_uid for event in self.search([('caldav_uid', '!=', False), ('create_uid', '=', user.id)])) - - for event in events: - ical = event.icalendar_instance - uid = ical.subcomponents[0]['UID'] - current_uids.discard(uid) # Remove from the set of current UIDs - self.sync_event_from_ical(ical, user) - - # Any UIDs remaining in current_uids are events that have been deleted on the CalDAV server - for uid in current_uids: - odoo_event = self.search([('caldav_uid', '=', uid), ('create_uid', '=', user.id)], limit=1) - if odoo_event: - odoo_event.unlink() - except Exception as e: - _logger.error(f"Failed to poll CalDAV server for user {user.name}: {e}") - - def sync_event_from_ical(self, ical, user): - event = ical.subcomponents[0] - uid = event['UID'] - start = event['DTSTART'].dt - end = event['DTEND'].dt - summary = event['SUMMARY'] - description = event.get('DESCRIPTION', '') - location = event.get('LOCATION', '') - - odoo_event = self.search([('caldav_uid', '=', uid), ('create_uid', '=', user.id)], limit=1) - values = { - 'name': summary, - 'start': start, - 'stop': end, - 'description': description, - 'location': location, - 'create_uid': user.id, - } - - if odoo_event: - odoo_event.write(values) - else: - values['caldav_uid'] = uid - self.create(values) - - def _get_caldav_client(self): - user = self.env.user - return caldav.DAVClient(url=user.caldav_server_url, username=user.caldav_username, password=user.caldav_password) - def _is_caldav_enabled(self): user = self.env.user - return bool(user.caldav_server_url and user.caldav_username and user.caldav_password and user.caldav_calendar_id) + return all([user.caldav_calendar_url, user.caldav_username, user.caldav_password]) - def _get_caldav_calendar_url(self): + def _get_caldav_client(self): user = self.env.user - return user.caldav_calendar_id.url + return caldav.DAVClient( + url=user.caldav_calendar_url, + username=user.caldav_username, + password=user.caldav_password + ) + + def sync_to_caldav(self): + if not self._is_caldav_enabled(): + return + client = self._get_caldav_client() + calendar = client.calendar(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: + caldav_event = calendar.add_event(ical_event) + event.caldav_uid = caldav_event.id + + def remove_from_caldav(self): + if not self._is_caldav_enabled(): + return + client = self._get_caldav_client() + calendar = client.calendar(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() def _get_icalendar(self): - cal = Calendar() - cal.add('prodid', '-//Odoo//') - cal.add('version', '2.0') + from icalendar import Calendar, Event + calendar = Calendar() + calendar.add('prodid', '-//Odoo//mxm.dk//') + calendar.add('version', '2.0') - event = Event() - event.add('summary', self.name) - event.add('dtstart', self.start) - event.add('dtend', self.stop) - event.add('dtstamp', self.create_date) - event.add('uid', self.caldav_uid) - event.add('description', self.description or '') - event.add('location', self.location or '') + 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) + calendar.add_component(ical_event) - # Add attendees - for attendee in self.attendee_ids: - vattendee = vCalAddress('MAILTO:%s' % attendee.email) - vattendee.params['cn'] = vText(attendee.partner_id.name) - vattendee.params['ROLE'] = vText('REQ-PARTICIPANT') - event.add('attendee', vattendee, encode=0) + return calendar.to_ical() - # Add organizer - organizer = self.create_uid.partner_id - if organizer: - vorganizer = vCalAddress('MAILTO:%s' % organizer.email) - vorganizer.params['cn'] = vText(organizer.name) - vorganizer.params['ROLE'] = vText('CHAIR') - event['organizer'] = vorganizer + @api.model + def poll_caldav_server(self): + users = self.env['res.users'].search([('caldav_calendar_url', '!=', False)]) + for user in users: + try: + self.with_user(user).poll_user_caldav_server() + except Exception as e: + _logger.error(f"Failed to poll CalDAV server for user {user.name}: {e}") - cal.add_component(event) + def poll_user_caldav_server(self): + if not self._is_caldav_enabled(): + return + client = self._get_caldav_client() + calendar = client.calendar(self.env.user.caldav_calendar_url) + events = calendar.events() + for caldav_event in events: + ical_event = caldav_event.icalendar_instance + self.sync_event_from_ical(ical_event) - return cal.to_ical().decode('utf-8') + 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) + if not event: + self.create({ + 'name': str(component.get('summary')), + 'start': component.decoded('dtstart'), + 'stop': component.decoded('dtend'), + 'description': str(component.get('description')), + 'location': str(component.get('location')), + 'caldav_uid': uid, + }) + else: + event.write({ + 'name': str(component.get('summary')), + 'start': component.decoded('dtstart'), + 'stop': component.decoded('dtend'), + 'description': str(component.get('description')), + 'location': str(component.get('location')), + }) diff --git a/caldav_sync/models/res_users.py b/caldav_sync/models/res_users.py index 2b240bd..b68e554 100644 --- a/caldav_sync/models/res_users.py +++ b/caldav_sync/models/res_users.py @@ -1,37 +1,8 @@ -from odoo import models, fields, api -import caldav -import logging - -_logger = logging.getLogger(__name__) +from odoo import models, fields class ResUsers(models.Model): _inherit = 'res.users' - caldav_server_url = fields.Char('CalDAV Server URL') - caldav_username = fields.Char('CalDAV Username') - caldav_password = fields.Char('CalDAV Password') - caldav_calendar_id = fields.Many2one('caldav.calendar', string='CalDAV Calendar') # Updated field - - @api.model - def _is_caldav_enabled(self): - self.ensure_one() - return bool(self.caldav_server_url and self.caldav_username and self.caldav_password and self.caldav_calendar_id) - - def fetch_caldav_calendars(self): - self.ensure_one() - client = caldav.DAVClient(url=self.caldav_server_url, username=self.caldav_username, password=self.caldav_password) - principal = client.principal() - calendars = principal.calendars() - caldav_calendar_model = self.env['caldav.calendar'] - caldav_calendar_model.search([('user_id', '=', self.id)]).unlink() # Clear existing calendars - for calendar in calendars: - caldav_calendar_model.create({ - 'user_id': self.id, - 'name': calendar.name, - 'url': str(calendar.url), - }) - return True - - def _get_caldav_client(self): - self.ensure_one() - return caldav.DAVClient(url=self.caldav_server_url, username=self.caldav_username, password=self.caldav_password) + caldav_calendar_url = fields.Char(string='CalDAV Calendar URL') + caldav_username = fields.Char(string='CalDAV Username') + caldav_password = fields.Char(string='CalDAV Password', password=True) diff --git a/caldav_sync/tests/test_caldav_sync.py b/caldav_sync/tests/test_caldav_sync.py index 09a210f..0ec64bf 100644 --- a/caldav_sync/tests/test_caldav_sync.py +++ b/caldav_sync/tests/test_caldav_sync.py @@ -1,4 +1,3 @@ - from odoo.tests.common import TransactionCase from unittest.mock import patch, MagicMock import caldav @@ -8,6 +7,7 @@ import logging _logger = logging.getLogger(__name__) + class TestCaldavSync(TransactionCase): def setUp(self): @@ -15,17 +15,11 @@ class TestCaldavSync(TransactionCase): self.user = self.env['res.users'].create({ 'name': 'Test User', 'login': 'testuser', - 'caldav_server_url': 'http://testserver/caldav', + 'caldav_calendar_url': 'http://test.calendar.url', 'caldav_username': 'testuser', 'caldav_password': 'password', }) self.env = self.env(context=dict(self.env.context, no_reset_password=True)) - self.calendar = self.env['caldav.calendar'].create({ - 'user_id': self.user.id, - 'name': 'Test Calendar', - 'url': 'http://testserver/caldav/calendars/testuser/calendar' - }) - self.user.write({'caldav_calendar_id': self.calendar.id}) @patch('odoo.addons.caldav_sync.models.calendar_event.CalendarEvent._get_caldav_client') def test_create_caldav_event(self, mock_get_caldav_client): @@ -140,4 +134,3 @@ class TestCaldavSync(TransactionCase): self.env['calendar.event'].poll_caldav_server() mock_logger.error.assert_any_call('Failed to poll CalDAV server for user Test User: Invalid credentials') - diff --git a/caldav_sync/views/res_users_views.xml b/caldav_sync/views/res_users_views.xml index 2745f71..02e34f0 100644 --- a/caldav_sync/views/res_users_views.xml +++ b/caldav_sync/views/res_users_views.xml @@ -8,17 +8,15 @@ - + -