first version of module passing its own tests
This commit is contained in:
parent
5988032dd5
commit
b96a1efca9
3 changed files with 150 additions and 131 deletions
|
|
@ -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'],
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue