caldav_sync: removed the ability to choose a calendar, use calendar url instead

This commit is contained in:
Marc Durepos 2024-05-23 13:11:00 -04:00
parent 7e3d05470f
commit 9d2ea9a8fa
6 changed files with 111 additions and 186 deletions

View file

@ -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.

View file

@ -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',

View file

@ -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')),
})

View file

@ -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)

View file

@ -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')

View file

@ -8,17 +8,15 @@
<xpath expr="//notebook" position="inside">
<page string="Calendar">
<group string="CalDAV">
<field name="caldav_server_url"/>
<field name="caldav_calendar_url"/>
<field name="caldav_username"/>
<field name="caldav_password"/>
<button name="fetch_caldav_calendars" type="object" string="Fetch Calendars"/>
<field name="caldav_calendar_id"/>
</group>
</page>
</xpath>
</field>
</record>
<record id="view_users_preferences_form" model="ir.ui.view">
<record id="view_users_form" model="ir.ui.view">
<field name="name">res.users.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
@ -26,11 +24,9 @@
<xpath expr="//notebook" position="inside">
<page string="Calendar">
<group string="CalDAV">
<field name="caldav_server_url"/>
<field name="caldav_calendar_url"/>
<field name="caldav_username"/>
<field name="caldav_password"/>
<button name="fetch_caldav_calendars" type="object" string="Fetch Calendars"/>
<field name="caldav_calendar_id"/>
</group>
</page>
</xpath>