From b96a1efca9ce88e7629ab9d1adafbb84fa62d9f7 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Thu, 23 May 2024 11:02:44 -0400 Subject: [PATCH] first version of module passing its own tests --- caldav_sync/__manifest__.py | 1 + caldav_sync/models/calendar_event.py | 60 ++++--- caldav_sync/tests/test_caldav_sync.py | 220 +++++++++++++------------- 3 files changed, 150 insertions(+), 131 deletions(-) diff --git a/caldav_sync/__manifest__.py b/caldav_sync/__manifest__.py index c3bb355..b1cf7d1 100644 --- a/caldav_sync/__manifest__.py +++ b/caldav_sync/__manifest__.py @@ -16,6 +16,7 @@ This module allows Odoo to synchronize calendar events with a CalDAV server. """, 'author': 'Your Name', + 'license': 'LGPL-3', 'depends': ['base', 'calendar'], 'external_dependencies': { 'python': ['caldav'], diff --git a/caldav_sync/models/calendar_event.py b/caldav_sync/models/calendar_event.py index 7a45b18..fb72e64 100644 --- a/caldav_sync/models/calendar_event.py +++ b/caldav_sync/models/calendar_event.py @@ -1,6 +1,7 @@ from odoo import models, fields, api import caldav import logging +from icalendar import Calendar, Event, vCalAddress, vText _logger = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class CalendarEvent(models.Model): @api.model def create(self, values): event = super(CalendarEvent, self).create(values) - if event._is_caldav_enabled(): + if event._is_caldav_enabled() and not self.env.context.get('skip_caldav_sync'): try: event.sync_to_caldav() except Exception as e: @@ -21,7 +22,7 @@ class CalendarEvent(models.Model): def write(self, values): result = super(CalendarEvent, self).write(values) - if self._is_caldav_enabled(): + if self._is_caldav_enabled() and not self.env.context.get('skip_caldav_sync'): try: self.sync_to_caldav() except Exception as e: @@ -41,8 +42,11 @@ class CalendarEvent(models.Model): if event._is_caldav_enabled(): client = event._get_caldav_client() calendar = client.principal().calendars()[0] # Assuming the first calendar - vevent = calendar.add_event(event._get_icalendar()) - event.caldav_uid = vevent.vobject_instance.vevent.uid.value + 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: @@ -70,7 +74,7 @@ class CalendarEvent(models.Model): 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() + 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) @@ -117,18 +121,34 @@ class CalendarEvent(models.Model): return bool(user.caldav_server_url and user.caldav_username and user.caldav_password) def _get_icalendar(self): - vevent = f""" -BEGIN:VCALENDAR -VERSION:2.0 -BEGIN:VEVENT -UID:{self.caldav_uid} -DTSTAMP:{self.start.strftime('%Y%m%dT%H%M%SZ')} -DTSTART:{self.start.strftime('%Y%m%dT%H%M%SZ')} -DTEND:{self.stop.strftime('%Y%m%dT%H%M%SZ')} -SUMMARY:{self.name} -DESCRIPTION:{self.description or ''} -LOCATION:{self.location or ''} -END:VEVENT -END:VCALENDAR -""" - return vevent + cal = Calendar() + cal.add('prodid', '-//Odoo//') + cal.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 '') + + # 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) + + # 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 + + cal.add_component(event) + + return cal.to_ical().decode('utf-8') diff --git a/caldav_sync/tests/test_caldav_sync.py b/caldav_sync/tests/test_caldav_sync.py index a1981da..b126f9f 100644 --- a/caldav_sync/tests/test_caldav_sync.py +++ b/caldav_sync/tests/test_caldav_sync.py @@ -1,7 +1,9 @@ - from odoo.tests.common import TransactionCase from unittest.mock import patch, MagicMock import caldav +from icalendar import Calendar, Event +from datetime import datetime + class TestCaldavSync(TransactionCase): @@ -16,127 +18,123 @@ class TestCaldavSync(TransactionCase): }) self.env = self.env(context=dict(self.env.context, no_reset_password=True)) - def test_create_caldav_event(self): + @patch('odoo.addons.caldav_sync.models.calendar_event.CalendarEvent._get_caldav_client') + def test_create_caldav_event(self, mock_get_caldav_client): + mock_client = MagicMock() + mock_principal = MagicMock() + mock_calendar = MagicMock() + mock_event = MagicMock() + mock_event.id = 'test-uid-12345' + + mock_client.principal.return_value = mock_principal + mock_principal.calendars.return_value = [mock_calendar] + mock_calendar.add_event.return_value = mock_event + mock_get_caldav_client.return_value = mock_client + + event = self.env['calendar.event'].with_user(self.user).create({ + 'name': 'Test Event', + 'start': '2024-05-22 10:00:00', + 'stop': '2024-05-22 11:00:00', + 'description': 'This is a test event', + 'location': 'Test Location', + 'create_uid': self.user.id, + }) + + self.assertEqual(event.name, 'Test Event') + self.assertEqual(event.caldav_uid, 'test-uid-12345') + + @patch('odoo.addons.caldav_sync.models.calendar_event.CalendarEvent._get_caldav_client') + @patch('odoo.addons.caldav_sync.models.calendar_event.CalendarEvent.sync_to_caldav') + def test_update_caldav_event(self, mock_sync_to_caldav, mock_get_caldav_client): + mock_client = MagicMock() + mock_principal = MagicMock() + mock_calendar = MagicMock() + mock_event = MagicMock() + mock_event.id = 'test-uid-12345' + + mock_client.principal.return_value = mock_principal + mock_principal.calendars.return_value = [mock_calendar] + mock_calendar.add_event.return_value = mock_event + mock_get_caldav_client.return_value = mock_client + + event = self.env['calendar.event'].with_user(self.user).create({ + 'name': 'Test Event', + 'start': '2024-05-22 10:00:00', + 'stop': '2024-05-22 11:00:00', + 'description': 'This is a test event', + 'location': 'Test Location', + 'create_uid': self.user.id, + }) + + event.with_user(self.user).write({ + 'name': 'Updated Test Event', + 'start': '2024-05-22 12:00:00', + 'stop': '2024-05-22 13:00:00', + }) + self.assertEqual(mock_sync_to_caldav.call_count, 2) + + self.assertEqual(event.name, 'Updated Test Event') + self.assertEqual(event.start, datetime.strptime('2024-05-22 12:00:00', '%Y-%m-%d %H:%M:%S')) + + @patch('odoo.addons.caldav_sync.models.calendar_event.CalendarEvent._get_caldav_client') + @patch('odoo.addons.caldav_sync.models.calendar_event.CalendarEvent.remove_from_caldav') + def test_delete_caldav_event(self, mock_remove_from_caldav, mock_get_caldav_client): + mock_client = MagicMock() + mock_principal = MagicMock() + mock_calendar = MagicMock() + mock_event = MagicMock() + mock_event.id = 'test-uid-12345' + + mock_client.principal.return_value = mock_principal + mock_principal.calendars.return_value = [mock_calendar] + mock_calendar.add_event.return_value = mock_event + mock_get_caldav_client.return_value = mock_client + + event = self.env['calendar.event'].with_user(self.user).create({ + 'name': 'Test Event', + 'start': '2024-05-22 10:00:00', + 'stop': '2024-05-22 11:00:00', + 'description': 'This is a test event', + 'location': 'Test Location', + 'create_uid': self.user.id, + }) + + event.with_user(self.user).unlink() + mock_remove_from_caldav.assert_called() + + @patch('odoo.addons.caldav_sync.models.calendar_event.CalendarEvent.sync_event_from_ical') + def test_poll_caldav_server(self, mock_sync_event_from_ical): + mock_sync_event_from_ical.return_value = None with patch('caldav.DAVClient') as MockClient: mock_client = MockClient.return_value mock_principal = mock_client.principal.return_value mock_calendar = mock_principal.calendars.return_value[0] mock_event = MagicMock() - mock_event.vobject_instance.vevent.uid.value = 'test-uid-12345' - mock_calendar.add_event.return_value = mock_event - event = self.env['calendar.event'].create({ - 'name': 'Test Event', - 'start': '2024-05-22 10:00:00', - 'stop': '2024-05-22 11:00:00', - 'description': 'This is a test event', - 'location': 'Test Location', - 'create_uid': self.user.id, - }) + # Create a Calendar object and add an Event to it + cal = Calendar() + event = Event() + event.add('uid', 'test-uid-12345') + event.add('dtstamp', datetime.strptime('20240522T100000Z', '%Y%m%dT%H%M%SZ')) + event.add('dtstart', datetime.strptime('20240522T100000Z', '%Y%m%dT%H%M%SZ')) + event.add('dtend', datetime.strptime('20240522T110000Z', '%Y%m%dT%H%M%SZ')) + event.add('summary', 'Polled Event') + event.add('description', 'This event was polled from CalDAV') + event.add('location', 'Polled Location') + cal.add_component(event) - self.assertEqual(event.name, 'Test Event') - self.assertEqual(event.caldav_uid, 'test-uid-12345') - - def test_update_caldav_event(self): - with patch('caldav.DAVClient') as MockClient: - mock_client = MockClient.return_value - mock_principal = mock_client.principal.return_value - mock_calendar = mock_principal.calendars.return_value[0] - mock_event = MagicMock() - mock_event.vobject_instance.vevent.uid.value = 'test-uid-12345' - mock_calendar.add_event.return_value = mock_event - - event = self.env['calendar.event'].create({ - 'name': 'Test Event', - 'start': '2024-05-22 10:00:00', - 'stop': '2024-05-22 11:00:00', - 'description': 'This is a test event', - 'location': 'Test Location', - 'create_uid': self.user.id, - }) - - with patch.object(event, 'sync_to_caldav') as mock_sync_to_caldav: - event.write({ - 'name': 'Updated Test Event', - 'start': '2024-05-22 12:00:00', - 'stop': '2024-05-22 13:00:00', - }) - mock_sync_to_caldav.assert_called_once() - - self.assertEqual(event.name, 'Updated Test Event') - self.assertEqual(event.start, '2024-05-22 12:00:00') - - def test_delete_caldav_event(self): - with patch('caldav.DAVClient') as MockClient: - mock_client = MockClient.return_value - mock_principal = mock_client.principal.return_value - mock_calendar = mock_principal.calendars.return_value[0] - mock_event = MagicMock() - mock_event.vobject_instance.vevent.uid.value = 'test-uid-12345' - mock_calendar.add_event.return_value = mock_event - - event = self.env['calendar.event'].create({ - 'name': 'Test Event', - 'start': '2024-05-22 10:00:00', - 'stop': '2024-05-22 11:00:00', - 'description': 'This is a test event', - 'location': 'Test Location', - 'create_uid': self.user.id, - }) - - with patch.object(event, 'remove_from_caldav') as mock_remove_from_caldav: - event.unlink() - mock_remove_from_caldav.assert_called_once() - - def test_poll_caldav_server(self): - with patch('caldav.DAVClient') as MockClient: - mock_client = MockClient.return_value - mock_principal = mock_client.principal.return_value - mock_calendar = mock_principal.calendars.return_value[0] - mock_event = MagicMock() - mock_event.icalendar.return_value = """ - BEGIN:VCALENDAR - VERSION:2.0 - BEGIN:VEVENT - UID:test-uid-12345 - DTSTAMP:20240522T100000Z - DTSTART:20240522T100000Z - DTEND:20240522T110000Z - SUMMARY:Polled Event - DESCRIPTION:This event was polled from CalDAV - LOCATION:Polled Location - END:VEVENT - END:VCALENDAR - """ + # Set the mock event's icalendar_instance to the iCal string + mock_event.icalendar_instance = Calendar.from_ical(cal.to_ical()) mock_calendar.events.return_value = [mock_event] - with patch.object(self.env['calendar.event'], 'sync_event_from_ical') as mock_sync_event_from_ical: - self.env['calendar.event'].poll_caldav_server() - mock_sync_event_from_ical.assert_called_once() + self.env['calendar.event'].poll_caldav_server() + mock_sync_event_from_ical.assert_called_once() - def test_poll_caldav_server_with_exception(self): + @patch('odoo.addons.caldav_sync.models.calendar_event._logger') + def test_poll_caldav_server_with_exception(self, mock_logger): with patch('caldav.DAVClient') as MockClient: mock_client = MockClient.return_value - mock_principal = mock_client.principal.return_value - mock_calendar = mock_principal.calendars.return_value[0] - mock_event = MagicMock() - mock_event.icalendar.return_value = """ - BEGIN:VCALENDAR - VERSION:2.0 - BEGIN:VEVENT - UID:test-uid-12345 - DTSTAMP:20240522T100000Z - DTSTART:20240522T100000Z - DTEND:20240522T110000Z - SUMMARY:Polled Event - DESCRIPTION:This event was polled from CalDAV - LOCATION:Polled Location - END:VEVENT - END:VCALENDAR - """ - mock_calendar.events.return_value = [mock_event] mock_client.principal.side_effect = Exception('Invalid credentials') - with patch.object(self.env['calendar.event'], '_logger') as mock_logger: - self.env['calendar.event'].poll_caldav_server() - mock_logger.error.assert_any_call('Failed to poll CalDAV server for user Test User: Invalid credentials') + self.env['calendar.event'].poll_caldav_server() + mock_logger.error.assert_any_call('Failed to poll CalDAV server for user Test User: Invalid credentials')