caldav_sync: Added some error handling for almost-correct calendar URLs.
This commit is contained in:
parent
8410bb64d7
commit
cd03903ea4
2 changed files with 195 additions and 139 deletions
|
|
@ -7,28 +7,28 @@
|
||||||
# For details, visit https://www.gnu.org/licenses/lgpl-3.0.en.html
|
# For details, visit https://www.gnu.org/licenses/lgpl-3.0.en.html
|
||||||
|
|
||||||
{
|
{
|
||||||
'name': 'CalDAV Synchronization',
|
"name": "CalDAV Synchronization",
|
||||||
'version': '17.0.0.5.3',
|
"version": "17.0.0.5.4",
|
||||||
'license': 'LGPL-3',
|
"license": "LGPL-3",
|
||||||
'category': 'Productivity',
|
"category": "Productivity",
|
||||||
'summary': 'Synchronize Odoo Calendar Events with CalDAV Servers',
|
"summary": "Synchronize Odoo Calendar Events with CalDAV Servers",
|
||||||
'description': """
|
"description": """
|
||||||
This module allows Odoo to synchronize calendar events with CalDAV servers.
|
This module allows Odoo to synchronize calendar events with CalDAV servers.
|
||||||
Users can synchronize their Odoo calendar with external applications like
|
Users can synchronize their Odoo calendar with external applications like
|
||||||
Apple Calendar or Thunderbird using CalDAV protocol.
|
Apple Calendar or Thunderbird using CalDAV protocol.
|
||||||
""",
|
""",
|
||||||
'author': 'Bemade Inc.',
|
"author": "Bemade Inc.",
|
||||||
'website': 'https://www.bemade.org',
|
"website": "https://www.bemade.org",
|
||||||
'depends': ['base', 'calendar'],
|
"depends": ["base", "calendar"],
|
||||||
'external_dependencies': {
|
"external_dependencies": {
|
||||||
'python': ['caldav', 'icalendar', 'bs4'],
|
"python": ["caldav", "icalendar", "bs4"],
|
||||||
},
|
},
|
||||||
'images': ['static/description/images/main_screenshot.png'],
|
"images": ["static/description/images/main_screenshot.png"],
|
||||||
'data': [
|
"data": [
|
||||||
'views/res_users_views.xml',
|
"views/res_users_views.xml",
|
||||||
'data/caldav_sync_data.xml',
|
"data/caldav_sync_data.xml",
|
||||||
],
|
],
|
||||||
'installable': True,
|
"installable": True,
|
||||||
'application': True,
|
"application": True,
|
||||||
'auto_install': False,
|
"auto_install": False,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import uuid
|
import uuid
|
||||||
from odoo import models, api, fields
|
from odoo import models, api, fields
|
||||||
|
from odoo.exceptions import UserError
|
||||||
import caldav
|
import caldav
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from icalendar import Calendar, Event, vCalAddress, vText, vWeekday
|
from icalendar import Calendar, Event, vCalAddress, vText
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from datetime import timedelta
|
|
||||||
import re
|
import re
|
||||||
from pytz import timezone, utc
|
from pytz import timezone, utc
|
||||||
|
|
||||||
|
|
@ -33,26 +33,26 @@ def _parse_rrule_string(rrule_str):
|
||||||
regex = re.compile(regex_str)
|
regex = re.compile(regex_str)
|
||||||
params_match = regex.search(rrule_str)
|
params_match = regex.search(rrule_str)
|
||||||
params_part = params_match.groups()[0]
|
params_part = params_match.groups()[0]
|
||||||
params = params_part.split(';')
|
params = params_part.split(";")
|
||||||
params_dict = {}
|
params_dict = {}
|
||||||
for param in params:
|
for param in params:
|
||||||
parts = param.split('=')
|
parts = param.split("=")
|
||||||
params_dict.update({parts[0]: try_to_int(parts[1])})
|
params_dict.update({parts[0]: try_to_int(parts[1])})
|
||||||
return params_dict
|
return params_dict
|
||||||
|
|
||||||
|
|
||||||
class CalendarEvent(models.Model):
|
class CalendarEvent(models.Model):
|
||||||
_inherit = 'calendar.event'
|
_inherit = "calendar.event"
|
||||||
|
|
||||||
caldav_uid = fields.Char(string='CalDAV UID', readonly=True)
|
caldav_uid = fields.Char(string="CalDAV UID", readonly=True)
|
||||||
caldav_recurrence_id = fields.Char(string='CalDAV Recurrence ID', readonly=True)
|
caldav_recurrence_id = fields.Char(string="CalDAV Recurrence ID", readonly=True)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
if not vals.get('caldav_uid'):
|
if not vals.get("caldav_uid"):
|
||||||
vals['caldav_uid'] = str(uuid.uuid4())
|
vals["caldav_uid"] = str(uuid.uuid4())
|
||||||
event = super(CalendarEvent, self).create(vals)
|
event = super(CalendarEvent, self).create(vals)
|
||||||
if not self.env.context.get('caldav_no_sync'):
|
if not self.env.context.get("caldav_no_sync"):
|
||||||
try:
|
try:
|
||||||
_logger.debug(f"Creating event {event.name} in CalDAV")
|
_logger.debug(f"Creating event {event.name} in CalDAV")
|
||||||
event.sync_create_to_caldav()
|
event.sync_create_to_caldav()
|
||||||
|
|
@ -62,7 +62,7 @@ class CalendarEvent(models.Model):
|
||||||
|
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
res = super(CalendarEvent, self).write(vals)
|
res = super(CalendarEvent, self).write(vals)
|
||||||
if not self.env.context.get('caldav_no_sync') and self.ids:
|
if not self.env.context.get("caldav_no_sync") and self.ids:
|
||||||
for rec in self:
|
for rec in self:
|
||||||
try:
|
try:
|
||||||
_logger.debug(f"Updating event {self.name} in CalDAV")
|
_logger.debug(f"Updating event {self.name} in CalDAV")
|
||||||
|
|
@ -72,7 +72,7 @@ class CalendarEvent(models.Model):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def unlink(self):
|
def unlink(self):
|
||||||
if not self.env.context.get('caldav_no_sync'):
|
if not self.env.context.get("caldav_no_sync"):
|
||||||
for event in self:
|
for event in self:
|
||||||
try:
|
try:
|
||||||
_logger.debug(f"Removing event {event.name} from CalDAV")
|
_logger.debug(f"Removing event {event.name} from CalDAV")
|
||||||
|
|
@ -89,7 +89,7 @@ class CalendarEvent(models.Model):
|
||||||
return caldav.DAVClient(
|
return caldav.DAVClient(
|
||||||
url=user.caldav_calendar_url,
|
url=user.caldav_calendar_url,
|
||||||
username=user.caldav_username,
|
username=user.caldav_username,
|
||||||
password=user.caldav_password
|
password=user.caldav_password,
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_create_to_caldav(self):
|
def sync_create_to_caldav(self):
|
||||||
|
|
@ -104,7 +104,9 @@ class CalendarEvent(models.Model):
|
||||||
caldav_event = calendar.add_event(ical_event)
|
caldav_event = calendar.add_event(ical_event)
|
||||||
caldav_uid = caldav_event.vobject_instance.vevent.uid.value
|
caldav_uid = caldav_event.vobject_instance.vevent.uid.value
|
||||||
_logger.debug(f"New CalDAV UID: {caldav_uid}")
|
_logger.debug(f"New CalDAV UID: {caldav_uid}")
|
||||||
event.with_context(caldav_no_sync=True).write({'caldav_uid': caldav_uid})
|
event.with_context(caldav_no_sync=True).write(
|
||||||
|
{"caldav_uid": caldav_uid}
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.error(f"Failed to sync event to CalDAV server: {e}")
|
_logger.error(f"Failed to sync event to CalDAV server: {e}")
|
||||||
|
|
||||||
|
|
@ -133,51 +135,60 @@ class CalendarEvent(models.Model):
|
||||||
caldav_event = calendar.object_by_uid(event.caldav_uid)
|
caldav_event = calendar.object_by_uid(event.caldav_uid)
|
||||||
caldav_event.delete()
|
caldav_event.delete()
|
||||||
except caldav.error.NotFoundError:
|
except caldav.error.NotFoundError:
|
||||||
_logger.warning(f"CalDAV event {event.caldav_uid} not found on server.")
|
_logger.warning(
|
||||||
|
f"CalDAV event {event.caldav_uid} not found on server."
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.error(f"Failed to remove event from CalDAV server: {e}")
|
_logger.error(f"Failed to remove event from CalDAV server: {e}")
|
||||||
|
|
||||||
def _get_icalendar(self):
|
def _get_icalendar(self):
|
||||||
calendar = Calendar()
|
calendar = Calendar()
|
||||||
calendar.add('prodid', '-//Odoo//mxm.dk//')
|
calendar.add("prodid", "-//Odoo//mxm.dk//")
|
||||||
calendar.add('version', '2.0')
|
calendar.add("version", "2.0")
|
||||||
|
|
||||||
for event in self:
|
for event in self:
|
||||||
user_tz = timezone('UTC')
|
user_tz = timezone("UTC")
|
||||||
if event.user_id.tz:
|
if event.user_id.tz:
|
||||||
user_tz = timezone(event.user_id.tz)
|
user_tz = timezone(event.user_id.tz)
|
||||||
ical_event = Event()
|
ical_event = Event()
|
||||||
ical_event.add('uid', event.caldav_uid)
|
ical_event.add("uid", event.caldav_uid)
|
||||||
ical_event.add('dtstamp', utc.localize(event.write_date).astimezone(user_tz))
|
ical_event.add(
|
||||||
|
"dtstamp", utc.localize(event.write_date).astimezone(user_tz)
|
||||||
|
)
|
||||||
if event.name:
|
if event.name:
|
||||||
ical_event.add('summary', event.name)
|
ical_event.add("summary", event.name)
|
||||||
if event.description and self._html_to_text(event.description):
|
if event.description and self._html_to_text(event.description):
|
||||||
ical_event.add('description', self._html_to_text(event.description))
|
ical_event.add("description", self._html_to_text(event.description))
|
||||||
if event.location:
|
if event.location:
|
||||||
ical_event.add('location', event.location)
|
ical_event.add("location", event.location)
|
||||||
if event.videocall_location:
|
if event.videocall_location:
|
||||||
ical_event.add('CONFERENCE', event.videocall_location)
|
ical_event.add("CONFERENCE", event.videocall_location)
|
||||||
for partner in event.partner_ids:
|
for partner in event.partner_ids:
|
||||||
if partner == event.user_id.partner_id:
|
if partner == event.user_id.partner_id:
|
||||||
continue
|
continue
|
||||||
attendee = vCalAddress(f'MAILTO:{partner.email}')
|
attendee = vCalAddress(f"MAILTO:{partner.email}")
|
||||||
attendee.params['cn'] = vText(partner.name)
|
attendee.params["cn"] = vText(partner.name)
|
||||||
attendee_record = self.env['calendar.attendee'].search([('event_id', '=', event.id), ('partner_id', '=', partner.id)], limit=1)
|
attendee_record = self.env["calendar.attendee"].search(
|
||||||
|
[("event_id", "=", event.id), ("partner_id", "=", partner.id)],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
if attendee_record:
|
if attendee_record:
|
||||||
attendee.params['partstat'] = vText(self._map_attendee_status(attendee_record.state))
|
attendee.params["partstat"] = vText(
|
||||||
ical_event.add('attendee', attendee, encode=0)
|
self._map_attendee_status(attendee_record.state)
|
||||||
|
)
|
||||||
|
ical_event.add("attendee", attendee, encode=0)
|
||||||
organizer = vCalAddress(f"MAILTO:{event.user_id.email}")
|
organizer = vCalAddress(f"MAILTO:{event.user_id.email}")
|
||||||
organizer.params['cn'] = event.user_id.name
|
organizer.params["cn"] = event.user_id.name
|
||||||
ical_event.add('organizer', organizer)
|
ical_event.add("organizer", organizer)
|
||||||
# Add RRULE if the event is recurrent
|
# Add RRULE if the event is recurrent
|
||||||
if event.recurrency and event.recurrence_id:
|
if event.recurrency and event.recurrence_id:
|
||||||
rrule = event.recurrence_id._get_rrule()
|
rrule = event.recurrence_id._get_rrule()
|
||||||
rrule_dict = _parse_rrule_string(str(rrule))
|
rrule_dict = _parse_rrule_string(str(rrule))
|
||||||
ical_event.add('rrule', rrule_dict)
|
ical_event.add("rrule", rrule_dict)
|
||||||
|
|
||||||
# Add DTSTART and DTEND
|
# Add DTSTART and DTEND
|
||||||
ical_event.add('dtstart', utc.localize(event.start).astimezone(user_tz))
|
ical_event.add("dtstart", utc.localize(event.start).astimezone(user_tz))
|
||||||
ical_event.add('dtend', utc.localize(event.stop).astimezone(user_tz))
|
ical_event.add("dtend", utc.localize(event.stop).astimezone(user_tz))
|
||||||
|
|
||||||
calendar.add_component(ical_event)
|
calendar.add_component(ical_event)
|
||||||
|
|
||||||
|
|
@ -185,7 +196,9 @@ class CalendarEvent(models.Model):
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def poll_caldav_server(self):
|
def poll_caldav_server(self):
|
||||||
all_users = self.env['res.users'].search([]).filtered(lambda u: u.is_caldav_enabled())
|
all_users = (
|
||||||
|
self.env["res.users"].search([]).filtered(lambda u: u.is_caldav_enabled())
|
||||||
|
)
|
||||||
for user in all_users:
|
for user in all_users:
|
||||||
self.with_user(user).poll_user_caldav_server()
|
self.with_user(user).poll_user_caldav_server()
|
||||||
|
|
||||||
|
|
@ -194,11 +207,23 @@ class CalendarEvent(models.Model):
|
||||||
if not self._is_caldav_enabled():
|
if not self._is_caldav_enabled():
|
||||||
return
|
return
|
||||||
client = self._get_caldav_client()
|
client = self._get_caldav_client()
|
||||||
calendar = client.calendar(url=self.env.user.caldav_calendar_url)
|
try:
|
||||||
events = calendar.events()
|
calendar = client.calendar(url=self.env.user.caldav_calendar_url)
|
||||||
|
events = calendar.events()
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(e)
|
||||||
|
principal = client.principal()
|
||||||
|
msg = f"""Failed to connect to the calendar, but successfully connected to the
|
||||||
|
server at {client.url}.
|
||||||
|
You may need to select another calendar URL from those below.
|
||||||
|
|
||||||
|
Available calendars:
|
||||||
|
|
||||||
|
"""
|
||||||
|
for calendar in principal.calendars():
|
||||||
|
msg += f"{calendar.name}: {calendar.url}\n"
|
||||||
|
raise UserError(msg)
|
||||||
caldav_uids = set()
|
caldav_uids = set()
|
||||||
now = datetime.now()
|
|
||||||
one_year_later = now + timedelta(days=365)
|
|
||||||
|
|
||||||
_logger.info(f"Polling CalDAV server for user {self.env.user.name}")
|
_logger.info(f"Polling CalDAV server for user {self.env.user.name}")
|
||||||
|
|
||||||
|
|
@ -207,40 +232,43 @@ class CalendarEvent(models.Model):
|
||||||
self.sync_event_from_ical(ical_event)
|
self.sync_event_from_ical(ical_event)
|
||||||
for component in ical_event.subcomponents:
|
for component in ical_event.subcomponents:
|
||||||
if isinstance(component, Event):
|
if isinstance(component, Event):
|
||||||
uid = str(component.get('uid'))
|
uid = str(component.get("uid"))
|
||||||
recurrence_id = str(component.get('recurrence-id'))
|
recurrence_id = str(component.get("recurrence-id"))
|
||||||
if recurrence_id == 'None':
|
if recurrence_id == "None":
|
||||||
recurrence_id = ''
|
recurrence_id = ""
|
||||||
caldav_uids.add(f"{uid}{recurrence_id}")
|
caldav_uids.add(f"{uid}{recurrence_id}")
|
||||||
|
|
||||||
_logger.info(f"CalDAV UIDs fetched: {caldav_uids}")
|
_logger.info(f"CalDAV UIDs fetched: {caldav_uids}")
|
||||||
|
|
||||||
# Remove Odoo events that no longer exist on the CalDAV server
|
# Remove Odoo events that no longer exist on the CalDAV server
|
||||||
odoo_events = self.search([('caldav_uid', '!=', False)])
|
odoo_events = self.search([("caldav_uid", "!=", False)])
|
||||||
for event in odoo_events:
|
for event in odoo_events:
|
||||||
recurrence_id = event.caldav_recurrence_id or ''
|
recurrence_id = event.caldav_recurrence_id or ""
|
||||||
event_uid = f"{event.caldav_uid}{event.caldav_recurrence_id or ''}"
|
event_uid = f"{event.caldav_uid}{event.caldav_recurrence_id or ''}"
|
||||||
if event_uid not in caldav_uids:
|
if event_uid not in caldav_uids:
|
||||||
_logger.info(f"Deleting orphan event {event.name} with UID {event.caldav_uid} "
|
_logger.info(
|
||||||
f"and Recurrence ID {event.caldav_recurrence_id or ''}")
|
f"Deleting orphan event {event.name} with UID {event.caldav_uid} "
|
||||||
|
f"and Recurrence ID {event.caldav_recurrence_id or ''}"
|
||||||
|
)
|
||||||
event.with_context(caldav_no_sync=True).unlink()
|
event.with_context(caldav_no_sync=True).unlink()
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_existing_instance(self, uid, recurrence_id):
|
def _get_existing_instance(self, uid, recurrence_id):
|
||||||
instance = self.env['calendar.event'].search([
|
instance = self.env["calendar.event"].search(
|
||||||
('caldav_uid', '=', uid),
|
[("caldav_uid", "=", uid), ("recurrence_id", "=", recurrence_id)]
|
||||||
('recurrence_id', '=', recurrence_id)
|
)
|
||||||
])
|
return instance or self.env["calendar.event"].search(
|
||||||
return instance or self.env['calendar.event'].search([
|
[
|
||||||
('caldav_uid', '=', 'uid'),
|
("caldav_uid", "=", "uid"),
|
||||||
('recurrence_id', '=', False),
|
("recurrence_id", "=", False),
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _get_recurrency_values_from_ical_event(self, component):
|
def _get_recurrency_values_from_ical_event(self, component):
|
||||||
""" Match the fields from calendar.event (recurring fields) to the fields specified in RRULE at
|
"""Match the fields from calendar.event (recurring fields) to the fields specified in RRULE at
|
||||||
https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html """
|
https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html"""
|
||||||
|
|
||||||
rrule = component.get('rrule')
|
rrule = component.get("rrule")
|
||||||
if not rrule:
|
if not rrule:
|
||||||
if not self.recurrency:
|
if not self.recurrency:
|
||||||
# No change, this was already not a recurring event
|
# No change, this was already not a recurring event
|
||||||
|
|
@ -249,50 +277,59 @@ class CalendarEvent(models.Model):
|
||||||
# This was a recurring event and has been made non-recurring
|
# This was a recurring event and has been made non-recurring
|
||||||
if self.recurrence_id.base_event_id != self:
|
if self.recurrence_id.base_event_id != self:
|
||||||
# This is not the base event, so change its recurrency only
|
# This is not the base event, so change its recurrency only
|
||||||
return {'recurrence_update': 'self_only', 'recurrency': False, 'follow_recurrence': False}
|
return {
|
||||||
|
"recurrence_update": "self_only",
|
||||||
|
"recurrency": False,
|
||||||
|
"follow_recurrence": False,
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
# This is the base event, so change all events in the list
|
# This is the base event, so change all events in the list
|
||||||
return {'recurrence_update': 'all_events', 'recurrency': False}
|
return {"recurrence_update": "all_events", "recurrency": False}
|
||||||
rrule_str = rrule.to_ical().decode('utf-8')
|
rrule_str = rrule.to_ical().decode("utf-8")
|
||||||
sequence = component.get('sequence')
|
sequence = component.get("sequence")
|
||||||
if sequence and sequence != 0:
|
if sequence and sequence != 0:
|
||||||
# This is not the base event so we can't change recurrence properties
|
# This is not the base event so we can't change recurrence properties
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
caldav_recurrence_id = component.get('recurrence-id')
|
caldav_recurrence_id = component.get("recurrence-id")
|
||||||
rrule_params = self.env['calendar.recurrence']._rrule_parse(rrule_str, component.decoded('dtstart'))
|
rrule_params = self.env["calendar.recurrence"]._rrule_parse(
|
||||||
|
rrule_str, component.decoded("dtstart")
|
||||||
|
)
|
||||||
vals = {
|
vals = {
|
||||||
'recurrency': True,
|
"recurrency": True,
|
||||||
'follow_recurrence': True,
|
"follow_recurrence": True,
|
||||||
'caldav_recurrence_id': caldav_recurrence_id,
|
"caldav_recurrence_id": caldav_recurrence_id,
|
||||||
'recurrence_update': 'all_events',
|
"recurrence_update": "all_events",
|
||||||
'rrule_type': rrule_params.get('rrule_type'),
|
"rrule_type": rrule_params.get("rrule_type"),
|
||||||
'end_type': rrule_params.get('end_type'),
|
"end_type": rrule_params.get("end_type"),
|
||||||
'interval': rrule_params.get('interval'),
|
"interval": rrule_params.get("interval"),
|
||||||
'count': rrule_params.get('count'),
|
"count": rrule_params.get("count"),
|
||||||
'month_by': rrule_params.get('monty_by'),
|
"month_by": rrule_params.get("monty_by"),
|
||||||
'day': rrule_params.get('day'),
|
"day": rrule_params.get("day"),
|
||||||
'byday': rrule_params.get('byday'),
|
"byday": rrule_params.get("byday"),
|
||||||
'until': rrule_params.get('until'),
|
"until": rrule_params.get("until"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if rrule_params.get('weekday'):
|
if rrule_params.get("weekday"):
|
||||||
vals.update(rrule_params.get('weekday'))
|
vals.update(rrule_params.get("weekday"))
|
||||||
day_list = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
day_list = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||||
vals.update({day: rrule_params.get(day) for day in day_list if day in rrule_params})
|
vals.update(
|
||||||
|
{day: rrule_params.get(day) for day in day_list if day in rrule_params}
|
||||||
|
)
|
||||||
|
|
||||||
return vals
|
return vals
|
||||||
|
|
||||||
def sync_event_from_ical(self, ical_event):
|
def sync_event_from_ical(self, ical_event):
|
||||||
email_regex = re.compile(r'[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+')
|
email_regex = re.compile(r"[a-z0-9.\-+_]+@[a-z0-9.\-+_]+\.[a-z]+")
|
||||||
current_user_email = self.env.user.email.lower()
|
current_user_email = self.env.user.email.lower()
|
||||||
|
|
||||||
for component in ical_event.subcomponents:
|
for component in ical_event.subcomponents:
|
||||||
if isinstance(component, Event):
|
if isinstance(component, Event):
|
||||||
uid = component.get('uid')
|
uid = component.get("uid")
|
||||||
recurrence_id = component.get(
|
recurrence_id = component.get(
|
||||||
'recurrence_id') # Unique identifier for a single event in a recurrence set
|
"recurrence_id"
|
||||||
attendees = component.get('attendee', [])
|
) # Unique identifier for a single event in a recurrence set
|
||||||
|
attendees = component.get("attendee", [])
|
||||||
|
|
||||||
if isinstance(attendees, vCalAddress):
|
if isinstance(attendees, vCalAddress):
|
||||||
attendees = [attendees]
|
attendees = [attendees]
|
||||||
|
|
@ -301,7 +338,8 @@ class CalendarEvent(models.Model):
|
||||||
|
|
||||||
attendees_emails = [
|
attendees_emails = [
|
||||||
email_regex.search(str(attendee)).group(0).lower().strip()
|
email_regex.search(str(attendee)).group(0).lower().strip()
|
||||||
for attendee in attendees if email_regex.search(str(attendee))
|
for attendee in attendees
|
||||||
|
if email_regex.search(str(attendee))
|
||||||
]
|
]
|
||||||
|
|
||||||
_logger.info(f"Attendees emails: {attendees_emails}")
|
_logger.info(f"Attendees emails: {attendees_emails}")
|
||||||
|
|
@ -310,59 +348,77 @@ class CalendarEvent(models.Model):
|
||||||
if current_user_email not in attendees_emails:
|
if current_user_email not in attendees_emails:
|
||||||
attendees_emails.append(current_user_email)
|
attendees_emails.append(current_user_email)
|
||||||
|
|
||||||
attendee_ids = self.env['res.partner'].search([('email', 'in', attendees_emails)])
|
attendee_ids = self.env["res.partner"].search(
|
||||||
|
[("email", "in", attendees_emails)]
|
||||||
|
)
|
||||||
|
|
||||||
existing_instance = self._get_existing_instance(uid, recurrence_id)
|
existing_instance = self._get_existing_instance(uid, recurrence_id)
|
||||||
start = component.decoded('dtstart')
|
start = component.decoded("dtstart")
|
||||||
if isinstance(start, datetime):
|
if isinstance(start, datetime):
|
||||||
start = start.astimezone(utc).replace(tzinfo=None)
|
start = start.astimezone(utc).replace(tzinfo=None)
|
||||||
end = component.decoded('dtend')
|
end = component.decoded("dtend")
|
||||||
if isinstance(end, datetime):
|
if isinstance(end, datetime):
|
||||||
end = end.astimezone(utc).replace(tzinfo=None)
|
end = end.astimezone(utc).replace(tzinfo=None)
|
||||||
values = {
|
values = {
|
||||||
'name': str(component.get('summary')),
|
"name": str(component.get("summary")),
|
||||||
'start': start,
|
"start": start,
|
||||||
'stop': end,
|
"stop": end,
|
||||||
'description': self._extract_component_text(component, 'description'),
|
"description": self._extract_component_text(
|
||||||
'location': self._extract_component_text(component, 'location'),
|
component, "description"
|
||||||
'videocall_location': self._extract_component_text(component, 'conference'),
|
),
|
||||||
'caldav_uid': uid,
|
"location": self._extract_component_text(component, "location"),
|
||||||
'partner_ids': [(6, 0, attendee_ids.ids)],
|
"videocall_location": self._extract_component_text(
|
||||||
|
component, "conference"
|
||||||
|
),
|
||||||
|
"caldav_uid": uid,
|
||||||
|
"partner_ids": [(6, 0, attendee_ids.ids)],
|
||||||
}
|
}
|
||||||
recurrency_vals = self._get_recurrency_values_from_ical_event(component)
|
recurrency_vals = self._get_recurrency_values_from_ical_event(component)
|
||||||
if recurrency_vals:
|
if recurrency_vals:
|
||||||
values.update(recurrency_vals)
|
values.update(recurrency_vals)
|
||||||
if not existing_instance:
|
if not existing_instance:
|
||||||
_logger.info(f"Creating with vals: {values}")
|
_logger.info(f"Creating with vals: {values}")
|
||||||
self.with_context({'caldav_no_sync': True}).create(values)
|
self.with_context({"caldav_no_sync": True}).create(values)
|
||||||
else:
|
else:
|
||||||
_logger.info(f"Updating with vals: {values}")
|
_logger.info(f"Updating with vals: {values}")
|
||||||
changed_vals = {}
|
changed_vals = {}
|
||||||
# Don't update partner_ids if no change
|
# Don't update partner_ids if no change
|
||||||
if attendee_ids - existing_instance.partner_ids:
|
if attendee_ids - existing_instance.partner_ids:
|
||||||
changed_vals.update({
|
changed_vals.update(
|
||||||
'partner_ids',values.pop('partner_ids'),
|
{
|
||||||
})
|
"partner_ids",
|
||||||
|
values.pop("partner_ids"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Don't write values that haven't changed
|
# Don't write values that haven't changed
|
||||||
for key, val in values.items():
|
for key, val in values.items():
|
||||||
if getattr(existing_instance, key) != val:
|
if getattr(existing_instance, key) != val:
|
||||||
changed_vals.update({key: values.pop(key)})
|
changed_vals.update({key: values.pop(key)})
|
||||||
if (recurrency_vals and recurrency_vals.get('recurrency')
|
if (
|
||||||
and (not existing_instance.recurrency or not
|
recurrency_vals
|
||||||
existing_instance.follow_recurrence)):
|
and recurrency_vals.get("recurrency")
|
||||||
existing_instance.write({
|
and (
|
||||||
'recurrency': True,
|
not existing_instance.recurrency
|
||||||
'follow_recurrence': True,
|
or not existing_instance.follow_recurrence
|
||||||
})
|
)
|
||||||
existing_instance.with_context({
|
):
|
||||||
'caldav_no_sync': True,
|
existing_instance.write(
|
||||||
}).write(changed_vals)
|
{
|
||||||
|
"recurrency": True,
|
||||||
|
"follow_recurrence": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
existing_instance.with_context(
|
||||||
|
{
|
||||||
|
"caldav_no_sync": True,
|
||||||
|
}
|
||||||
|
).write(changed_vals)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_component_text(component, subcomponent_name):
|
def _extract_component_text(component, subcomponent_name):
|
||||||
text = str(component.get(subcomponent_name))
|
text = str(component.get(subcomponent_name))
|
||||||
text = text if text != 'None' else ''
|
text = text if text != "None" else ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _html_to_text(html):
|
def _html_to_text(html):
|
||||||
|
|
@ -371,19 +427,19 @@ class CalendarEvent(models.Model):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _map_attendee_status(state):
|
def _map_attendee_status(state):
|
||||||
mapping = {
|
mapping = {
|
||||||
'needsAction': 'NEEDS-ACTION',
|
"needsAction": "NEEDS-ACTION",
|
||||||
'accepted': 'ACCEPTED',
|
"accepted": "ACCEPTED",
|
||||||
'declined': 'DECLINED',
|
"declined": "DECLINED",
|
||||||
'tentative': 'TENTATIVE',
|
"tentative": "TENTATIVE",
|
||||||
}
|
}
|
||||||
return mapping.get(state, 'NEEDS-ACTION')
|
return mapping.get(state, "NEEDS-ACTION")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _map_ical_status(ical_status):
|
def _map_ical_status(ical_status):
|
||||||
mapping = {
|
mapping = {
|
||||||
'NEEDS-ACTION': 'needsAction',
|
"NEEDS-ACTION": "needsAction",
|
||||||
'ACCEPTED': 'accepted',
|
"ACCEPTED": "accepted",
|
||||||
'DECLINED': 'declined',
|
"DECLINED": "declined",
|
||||||
'TENTATIVE': 'tentative',
|
"TENTATIVE": "tentative",
|
||||||
}
|
}
|
||||||
return mapping.get(ical_status, 'needsAction')
|
return mapping.get(ical_status, "needsAction")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue