first version of module passing its own tests

This commit is contained in:
Marc Durepos 2024-05-23 11:02:44 -04:00
parent 5988032dd5
commit b96a1efca9
3 changed files with 150 additions and 131 deletions

View file

@ -16,6 +16,7 @@
This module allows Odoo to synchronize calendar events with a CalDAV server. This module allows Odoo to synchronize calendar events with a CalDAV server.
""", """,
'author': 'Your Name', 'author': 'Your Name',
'license': 'LGPL-3',
'depends': ['base', 'calendar'], 'depends': ['base', 'calendar'],
'external_dependencies': { 'external_dependencies': {
'python': ['caldav'], 'python': ['caldav'],

View file

@ -1,6 +1,7 @@
from odoo import models, fields, api from odoo import models, fields, api
import caldav import caldav
import logging import logging
from icalendar import Calendar, Event, vCalAddress, vText
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -12,7 +13,7 @@ class CalendarEvent(models.Model):
@api.model @api.model
def create(self, values): def create(self, values):
event = super(CalendarEvent, self).create(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: try:
event.sync_to_caldav() event.sync_to_caldav()
except Exception as e: except Exception as e:
@ -21,7 +22,7 @@ class CalendarEvent(models.Model):
def write(self, values): def write(self, values):
result = super(CalendarEvent, self).write(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: try:
self.sync_to_caldav() self.sync_to_caldav()
except Exception as e: except Exception as e:
@ -41,8 +42,11 @@ class CalendarEvent(models.Model):
if event._is_caldav_enabled(): if event._is_caldav_enabled():
client = event._get_caldav_client() client = event._get_caldav_client()
calendar = client.principal().calendars()[0] # Assuming the first calendar calendar = client.principal().calendars()[0] # Assuming the first calendar
vevent = calendar.add_event(event._get_icalendar()) caldav_event = calendar.add_event(event._get_icalendar())
event.caldav_uid = vevent.vobject_instance.vevent.uid.value 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): def remove_from_caldav(self):
for event in 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)])) current_uids = set(event.caldav_uid for event in self.search([('caldav_uid', '!=', False), ('create_uid', '=', user.id)]))
for event in events: for event in events:
ical = event.icalendar() ical = event.icalendar_instance
uid = ical.subcomponents[0]['UID'] uid = ical.subcomponents[0]['UID']
current_uids.discard(uid) # Remove from the set of current UIDs current_uids.discard(uid) # Remove from the set of current UIDs
self.sync_event_from_ical(ical, user) 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) return bool(user.caldav_server_url and user.caldav_username and user.caldav_password)
def _get_icalendar(self): def _get_icalendar(self):
vevent = f""" cal = Calendar()
BEGIN:VCALENDAR cal.add('prodid', '-//Odoo//')
VERSION:2.0 cal.add('version', '2.0')
BEGIN:VEVENT
UID:{self.caldav_uid} event = Event()
DTSTAMP:{self.start.strftime('%Y%m%dT%H%M%SZ')} event.add('summary', self.name)
DTSTART:{self.start.strftime('%Y%m%dT%H%M%SZ')} event.add('dtstart', self.start)
DTEND:{self.stop.strftime('%Y%m%dT%H%M%SZ')} event.add('dtend', self.stop)
SUMMARY:{self.name} event.add('dtstamp', self.create_date)
DESCRIPTION:{self.description or ''} event.add('uid', self.caldav_uid)
LOCATION:{self.location or ''} event.add('description', self.description or '')
END:VEVENT event.add('location', self.location or '')
END:VCALENDAR
""" # Add attendees
return vevent 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')

View file

@ -1,7 +1,9 @@
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
import caldav import caldav
from icalendar import Calendar, Event
from datetime import datetime
class TestCaldavSync(TransactionCase): class TestCaldavSync(TransactionCase):
@ -16,127 +18,123 @@ class TestCaldavSync(TransactionCase):
}) })
self.env = self.env(context=dict(self.env.context, no_reset_password=True)) 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: with patch('caldav.DAVClient') as MockClient:
mock_client = MockClient.return_value mock_client = MockClient.return_value
mock_principal = mock_client.principal.return_value mock_principal = mock_client.principal.return_value
mock_calendar = mock_principal.calendars.return_value[0] mock_calendar = mock_principal.calendars.return_value[0]
mock_event = MagicMock() 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({ # Create a Calendar object and add an Event to it
'name': 'Test Event', cal = Calendar()
'start': '2024-05-22 10:00:00', event = Event()
'stop': '2024-05-22 11:00:00', event.add('uid', 'test-uid-12345')
'description': 'This is a test event', event.add('dtstamp', datetime.strptime('20240522T100000Z', '%Y%m%dT%H%M%SZ'))
'location': 'Test Location', event.add('dtstart', datetime.strptime('20240522T100000Z', '%Y%m%dT%H%M%SZ'))
'create_uid': self.user.id, 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') # Set the mock event's icalendar_instance to the iCal string
self.assertEqual(event.caldav_uid, 'test-uid-12345') mock_event.icalendar_instance = Calendar.from_ical(cal.to_ical())
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
"""
mock_calendar.events.return_value = [mock_event] 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()
self.env['calendar.event'].poll_caldav_server() mock_sync_event_from_ical.assert_called_once()
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: with patch('caldav.DAVClient') as MockClient:
mock_client = MockClient.return_value 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') 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()
self.env['calendar.event'].poll_caldav_server() mock_logger.error.assert_any_call('Failed to poll CalDAV server for user Test User: Invalid credentials')
mock_logger.error.assert_any_call('Failed to poll CalDAV server for user Test User: Invalid credentials')