caldav_sync: major rework underway. This commit is broken.

This commit is contained in:
Marc Durepos 2024-11-04 07:48:48 -05:00
parent 6b3b76e9e7
commit 0104e3897c
3 changed files with 406 additions and 242 deletions

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import calendar_event
from . import res_users
from . import calendar_recurrence

View file

@ -1,8 +1,11 @@
import uuid
from wsgiref.util import request_uri
from odoo import models, api, fields, Command
from odoo.addons.calendar.models.calendar_recurrence import MAX_RECURRENT_EVENT
import caldav
import logging
from datetime import datetime
from datetime import datetime, date
from icalendar import Calendar, Event, vCalAddress, vText
from bs4 import BeautifulSoup
import re
@ -50,11 +53,60 @@ class CalendarEvent(models.Model):
_inherit = "calendar.event"
caldav_uid = fields.Char(string="CalDAV UID", readonly=True)
caldav_recurrence_id = fields.Char(string="CalDAV Recurrence ID", readonly=True)
# Recurrence ID in iCalendar is the date or datetime the event would have
# been at if it followed the sequence. It is set by calendar.recurrence
# when applying a recurrence.
caldav_recurrence_id = fields.Char(
string="CalDAV Recurrence ID",
)
caldav_user_ids = fields.Many2many(
comodel_name="res.users",
compute="_compute_caldav_users",
)
is_base_event = fields.Boolean(compute="_compute_is_base_event")
differs_from_base_event = fields.Boolean(compute="_compute_differs_from_base_event")
@api.depends("name", "description", "partner_ids", "location", "videocall_location")
def _compute_differs_from_base_event(self):
base_events = self.filtered("is_base_event")
non_base_events = self - base_events
for event in base_events:
event.differs_from_base_event = False
fields_to_check = [
"name",
"description",
"partner_ids",
"location",
"videocall_location",
]
for event in non_base_events:
base_event = event.recurrence_id.base_event_id
event.differs_from_base_event = any(
[
getattr(event, field) != getattr(base_event, field)
for field in fields_to_check
]
)
@api.depends("is_base_event")
def _compute_update_all_recurrence(self):
for rec in self:
rec.update_all_recurrence = (
rec.recurrency
and rec.is_base_event
and (
rec.recurrence_update == "all_events"
or not rec.recurrence_update
or rec.recurrence_id.calendar_event_ids == rec
)
)
@api.depends("recurrency", "recurrence_id.base_event_id")
def _compute_is_base_event(self):
for rec in self:
rec.is_base_event = (
not rec.recurrency or rec.recurrence_id.base_event_id == rec
)
@api.depends("user_id", "partner_ids", "partner_ids.user_id")
def _compute_caldav_users(self):
@ -63,6 +115,12 @@ class CalendarEvent(models.Model):
"is_caldav_enabled"
)
def _to_sync(self):
return self.filtered(
lambda event: event._is_caldav_enabled()
and (event.is_base_event or event.differs_from_base_event)
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
@ -70,23 +128,23 @@ class CalendarEvent(models.Model):
vals["caldav_uid"] = str(uuid.uuid4())
events = super(CalendarEvent, self).create(vals_list)
if not self.env.context.get("caldav_no_sync"):
events._sync_create_to_caldav()
events._to_sync()._sync_create_to_caldav()
return events
def write(self, vals):
res = super(CalendarEvent, self).write(vals)
if not self.env.context.get("caldav_no_sync") and self.ids:
for rec in self.filtered(lambda event: event._is_caldav_enabled()):
try:
_logger.debug(f"Updating event {self.name} in CalDAV")
rec._sync_update_to_caldav()
except Exception as e:
_logger.error(f"Failed to update event in CalDAV server: {e}")
res = super().write(vals)
to_sync = self._to_sync()
if to_sync and self.env.context.get("caldav_no_sync"):
for rec in to_sync:
_logger.debug(
f"Updating event {rec.name} in CalDAV. " f"{rec.fields_get()}"
)
rec._sync_update_to_caldav()
return res
def unlink(self):
if not self.env.context.get("caldav_no_sync"):
for rec in self.filtered(lambda event: event._is_caldav_enabled()):
for rec in self._to_sync():
try:
_logger.debug(f"Removing event {rec.name} from CalDAV")
rec._sync_remove_from_caldav()
@ -99,107 +157,146 @@ class CalendarEvent(models.Model):
def _sync_create_to_caldav(self):
for event in self:
ical_event = event._get_icalendar()
for user in event.caldav_user_ids:
client = user._get_caldav_client()
calendar = client.calendar(url=user.caldav_calendar_url)
try:
_logger.debug(f"Creating new CalDAV event for {event.name}")
caldav_event = calendar.add_event(ical_event)
caldav_uid = caldav_event.vobject_instance.vevent.uid.value
_logger.debug(f"New CalDAV UID: {caldav_uid}")
event.with_context(caldav_no_sync=True).write(
{"caldav_uid": caldav_uid}
)
caldav_events = event._create_in_icalendar(calendar)
for caldav_event in caldav_events:
caldav_uid = caldav_event.vobject_instance.vevent.uid.value
_logger.debug(f"New CalDAV UID: {caldav_uid}")
event.with_context(caldav_no_sync=True).write(
{"caldav_uid": caldav_uid}
)
except Exception as e:
_logger.error(f"Failed to sync event to CalDAV server: {e}")
def _sync_update_to_caldav(self):
for event in self:
ical_event = event._get_icalendar()
for user in event.caldav_user_ids:
ical_event_data = self._create_event_data()
for user in self.caldav_user_ids:
client = user._get_caldav_client()
calendar = client.calendar(url=self.env.user.caldav_calendar_url)
event = self._find_in_icalendar(calendar, user)
if event:
event.save(**ical_event_data)
else:
calendar.save_event(**ical_event_data)
def _sync_remove_from_caldav(self):
if self.caldav_uid:
for user in self.caldav_user_ids:
client = user._get_caldav_client()
calendar = client.calendar(url=self.env.user.caldav_calendar_url)
try:
_logger.debug(f"Updating existing CalDAV event {event.caldav_uid}")
calendar.save_event(ical=ical_event)
except Exception as e:
_logger.error(f"Failed to sync event to CalDAV server: {e}")
def _sync_remove_from_caldav(self):
for event in self:
if event.caldav_uid:
for user in event.caldav_user_ids:
client = user._get_caldav_client()
calendar = client.calendar(url=self.env.user.caldav_calendar_url)
try:
_logger.debug(f"Removing CalDAV event {event.caldav_uid}")
calendar.event_by_uid(event.caldav_uid).delete()
# event._get_icalendar().delete()
except caldav.error.NotFoundError:
_logger.warning(
f"CalDAV event {event.caldav_uid} not found on server."
)
except Exception as e:
_logger.error(f"Failed to remove event from CalDAV server: {e}")
def _get_icalendar(self):
calendar = Calendar()
calendar.add("prodid", "-//Odoo//mxm.dk//")
calendar.add("version", "2.0")
for event in self:
user_tz = timezone("UTC")
if event.user_id.tz:
user_tz = timezone(event.user_id.tz)
ical_event = Event()
ical_event.add("uid", event.caldav_uid)
ical_event.add("dtstamp", utc.localize(datetime.now()).astimezone(user_tz))
ical_event.add(
"last-modified", utc.localize(self.write_date).astimezone(user_tz)
)
ical_event.add(
"created", utc.localize(self.create_date).astimezone(user_tz)
)
if event.name:
ical_event.add("summary", event.name)
# TODO: Consider using X-ALT-DESC to stick HTML into the iCal event desc.
if event.description and self._html_to_text(event.description):
ical_event.add("description", self._html_to_text(event.description))
if event.location:
ical_event.add("location", event.location)
if event.videocall_location:
ical_event.add("CONFERENCE", event.videocall_location)
for partner in event.partner_ids:
if partner == event.user_id.partner_id:
continue
attendee = vCalAddress(f"MAILTO:{partner.email}")
attendee.params["cn"] = vText(partner.name)
attendee_record = self.env["calendar.attendee"].search(
[("event_id", "=", event.id), ("partner_id", "=", partner.id)],
limit=1,
)
if attendee_record:
attendee.params["partstat"] = vText(
self._map_attendee_status(attendee_record.state)
_logger.debug(f"Removing CalDAV event {self.caldav_uid}")
ical_event = self._find_in_icalendar(calendar, user)
if ical_event:
ical_event.delete()
except caldav.error.NotFoundError:
_logger.warning(
f"CalDAV event {self.caldav_uid} not found on server during deletion."
)
ical_event.add(name="attendee", value=attendee, encode=False)
organizer = vCalAddress(f"MAILTO:{event.user_id.email}")
organizer.params["cn"] = event.user_id.name
ical_event.add("organizer", organizer)
# Add RRULE if the event is recurrent
if event.recurrency and event.recurrence_id:
rrule = event.recurrence_id._get_rrule()
rrule_dict = _parse_rrule_string(str(rrule))
ical_event.add("rrule", rrule_dict)
except Exception as e:
_logger.error(f"Failed to remove event from CalDAV server: {e}")
# Add DTSTART and DTEND
ical_event.add("dtstart", utc.localize(event.start).astimezone(user_tz))
ical_event.add("dtend", utc.localize(event.stop).astimezone(user_tz))
def _find_in_icalendar(self, calendar, user):
# Since search by uid and recurrence-id is not yet supported,
# we do it manually
match_recurrence_id = self.recurrency and not self.is_base_event
if not match_recurrence_id:
return calendar.event_by_uid(self.caldav_uid)
else:
events = user._get_caldav_events()
for event in events:
event = event.icalendar_instance
if (
event.get("uid") == self.caldav_uid
and event.get("recurrence-id") == self.caldav_recurrence_id
):
return event
return None
calendar.add_component(ical_event)
def _create_in_icalendar(self, calendar):
ical_event_data = self._create_event_data()
caldav_event = calendar.save_event(**ical_event_data)
if self.recurrency and self.is_base_event and not self.follow_recurrence:
ical_event_data = self._create_event_data()
second_caldav_event = calendar.save_event(**ical_event_data)
return [caldav_event, second_caldav_event]
return [caldav_event]
return calendar.to_ical()
def _create_event_data(self):
event_data = {}
self._add_event_dates(event_data)
self._add_event_header_info(event_data)
self._add_event_attendees(event_data)
if self.is_base_event and self.recurrency:
self._add_event_recurrence(event_data)
elif self.recurrency:
self._add_event_recurrence_id(event_data)
return event_data
def _add_event_header_info(self, event_data):
event_data["uid"] = self.caldav_uid
if self.name:
event_data["summary"] = self.name
# TODO: Consider using X-ALT-DESC to stick HTML into the iCal event desc.
if self.description and self._html_to_text(self.description):
event_data["description"] = self._html_to_text(self.description)
if self.location:
event_data["location"] = self.location
if self.videocall_location:
event_data["CONFERENCE"] = self.videocall_location
def _add_event_dates(self, event_data):
user_tz = timezone("UTC")
if self.user_id.tz:
user_tz = timezone(self.user_id.tz)
event_data["last-modified"] = utc.localize(self.write_date).astimezone(user_tz)
event_data["created"] = utc.localize(self.create_date).astimezone(user_tz)
event_data["dtstart"] = utc.localize(self.start).astimezone(user_tz)
event_data["dtend"] = utc.localize(self.stop).astimezone(user_tz)
return event_data
def _add_event_recurrence_id(self, event_data):
if self.recurrency:
event_data["recurrence-id"] = self._get_ical_recurrence_id()
def _add_event_recurrence(self, event_data):
# Add RRULE if the event is recurrent
if self.recurrency:
rrule = self.recurrence_id._get_rrule()
rrule_dict = _parse_rrule_string(str(rrule))
event_data["rrule"] = rrule_dict
def _add_event_attendees(self, event_data):
attendee_lines = []
for partner in self.partner_ids:
if partner == self.user_id.partner_id:
continue
attendee = vCalAddress(f"MAILTO:{partner.email}")
attendee.params["cn"] = vText(partner.name)
attendee_record = self.env["calendar.attendee"].search(
[("event_id", "=", self.id), ("partner_id", "=", partner.id)],
limit=1,
)
if attendee_record:
attendee.params["partstat"] = vText(
self._map_attendee_status(attendee_record.state)
)
attendee_lines.append(attendee)
organizer = vCalAddress(f"MAILTO:{self.user_id.email}")
organizer.params["cn"] = self.user_id.name
event_data["organizer"] = organizer
event_data["attendee"] = attendee_lines
def _get_ical_recurrence_id(self):
user_tz = timezone(self.user_id.tz)
recurrence_id = utc.localize(
datetime.strptime(self.caldav_recurrence_id, "%Y%m%dT%H%M%S")
).astimezone(user_tz)
return recurrence_id
@api.model
def poll_caldav_server(self):
@ -211,33 +308,59 @@ class CalendarEvent(models.Model):
def _poll_user_caldav_server(self, user):
_logger.info(f"Polling CalDAV server for user {user.name}")
events = user._get_caldav_events()
caldav_uids = set()
synced_events = self.env["calendar.event"]
for caldav_event in events:
ical_event = caldav_event.icalendar_instance
caldav_uids |= self._sync_event_from_ical(ical_event, user)
synced_events |= self._sync_event_from_ical(ical_event, user)
_logger.info(f"CalDAV UIDs fetched: {caldav_uids}")
# Remove Odoo events that no longer exist on the CalDAV server
# TODO: check if this fails when the user is deleting someone else's event
# TODO: check if we should send updates to invitees
odoo_events = self.search([("caldav_uid", "!=", False)])
for event in odoo_events:
recurrence_id = event.caldav_recurrence_id or ""
event_uid = f"{event.caldav_uid}{event.caldav_recurrence_id or ''}"
if event_uid not in caldav_uids:
_logger.info(
f"Deleting orphan event {event.name} with UID {event.caldav_uid} "
f"and Recurrence ID {recurrence_id}"
)
event.with_context(caldav_no_sync=True).with_user(user).unlink()
orphaned_events = self.search(
[
("caldav_uid", "!=", False),
("id", "not " "in", synced_events.ids),
("user_id", "=", user.id),
]
)._to_sync()
if orphaned_events:
_logger.info(f"Deleting orphaned events {orphaned_events.ids}")
orphaned_events.with_context(caldav_no_sync=True).with_user(user).unlink()
@api.model
def _get_existing_instance(self, uid, recurrence_id):
instance = self.env["calendar.event"].search(
[("caldav_uid", "=", uid), ("recurrence_id", "=", recurrence_id)]
)
if recurrence_id:
recurrence_id = recurrence_id.dt.strftime("%Y%m%dT%H%M%S")
instance = self.env["calendar.event"].search(
[
("caldav_uid", "=", uid),
("caldav_recurrence_id", "=", recurrence_id),
]
)
else:
instance = self.env["calendar.event"].search(
[
("caldav_uid", "=", uid),
("recurrency", "=", False),
]
)
if instance:
return instance
else:
return (
self.env["calendar.recurrence"]
.search(
[
("base_event_id.caldav_uid", "=", uid),
]
)
.base_event_id
)
if len(instance) == 1:
return instance
if len(instance) > 1:
instance = instance.recurrence_id.base_event_id
return instance or self.env["calendar.event"].search(
[
("caldav_uid", "=", "uid"),
@ -249,169 +372,178 @@ class CalendarEvent(models.Model):
"""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"""
if component.get("recurrence-id"):
# When a component has "recurrence-id" set, it is a single event
# in a series of events. Recurrence-id is the time the event should
# normally occur at if it follows the recurrence of the series.
follow_recurrence = (
component.get("recurrence-id").dt == component.get("dtstart").dt
)
if self and self.follow_recurrence == follow_recurrence:
return {
"follow_recurrence": follow_recurrence,
}
else:
return {
"follow_recurrence": follow_recurrence,
"recurrence_update": "self-only",
}
rrule = [item[1] for item in component.property_items() if item[0] == "RRULE"]
rrule = rrule[0] if rrule else None
if not rrule:
if not self.recurrency:
# No change, this was already not a recurring event
return {}
else:
# This was a recurring event and has been made non-recurring
if self.recurrence_id.base_event_id != self:
# This is not the base event, so change its recurrency only
return {
"recurrence_update": "self_only",
"recurrency": False,
"follow_recurrence": False,
}
else:
# This is the base event, so change all events in the list
return {"recurrence_update": "all_events", "recurrency": False}
rrule_str = rrule.to_ical() and rrule.to_ical().decode("utf-8")
sequence = component.get("sequence")
if sequence and sequence != 1:
# This is not the base event so we can't change recurrence properties
return {}
caldav_recurrence_id = component.get("recurrence-id")
rrule_str = rrule.to_ical() and rrule.to_ical().decode("utf-8")
rrule_params = self.env["calendar.recurrence"]._rrule_parse(
rrule_str, component.decoded("dtstart")
)
vals = {
"recurrency": True,
"follow_recurrence": True,
"caldav_recurrence_id": caldav_recurrence_id,
"recurrence_update": "all_events",
"rrule_type": rrule_params.get("rrule_type"),
"end_type": rrule_params.get("end_type"),
"interval": rrule_params.get("interval"),
"count": rrule_params.get("count"),
"month_by": rrule_params.get("monty_by"),
"day": rrule_params.get("day"),
"byday": rrule_params.get("byday"),
"until": rrule_params.get("until"),
**rrule_params,
}
# Convert None to False since fields from Odoo that are not filled come back False
vals = {
key: value if value is not None else False for key, value in vals.items()
}
if rrule_params.get("weekday"):
vals.update(rrule_params.get("weekday"))
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}
)
# Forever doesn't exist in Odoo. The calendar.recurrence model changes 'forever'
# into 'count' with MAX_RECURRENT_EVENT as the 'count' parameter
if vals.get("end_type") == "forever":
vals.update(end_type="count")
if not vals.get("count"):
vals.update(count=MAX_RECURRENT_EVENT)
return vals
def _get_recurrence_changes(self, recurrency_vals):
if not recurrency_vals and not self.recurrency:
return {}
if not recurrency_vals and self.recurrency:
return {"recurrence_update": "all_events", "recurrency": False}
if recurrency_vals and not self.recurrency:
return recurrency_vals
changed_fields = {
key: recurrency_vals[key]
for key in recurrency_vals.keys()
if hasattr(self, key) and recurrency_vals[key] != getattr(self, key)
}
if (
len(changed_fields) == 1
and changed_fields.get("recurrence_update") == "all_events"
):
return {}
return changed_fields
def _get_value_changes(self, values):
changed_vals = {}
# Don't update partner_ids if no change
if "partner_ids" in values:
partner_ids = values["partner_ids"][0][2] # this is a SET command
added_partner_ids = set(
[id for id in partner_ids if id not in self.partner_ids.ids]
)
removed_partner_ids = set(
[id for id in self.partner_ids.ids if id not in partner_ids]
)
if not (added_partner_ids or removed_partner_ids):
values.pop("partner_ids") # They break the equality check later
else:
changed_vals["partner_ids"] = values["partner_ids"]
# Get just the list of values that have changed, leave the others alone
for key, val in values.items():
curr_val = getattr(self, key)
# Can't deal with x2many fields, need ID from a record
if isinstance(val, list):
continue
if curr_val and isinstance(curr_val, models.Model):
if len(curr_val) > 1:
continue
curr_val = curr_val.id
if curr_val != val:
changed_vals.update({key: val})
return changed_vals
def _sync_event_from_ical(self, ical_event, user):
current_user_email = user.email.lower()
caldav_uids = set()
synced_events = self.env["calendar.event"]
event_components = [
component for component in ical_event.walk() if component.name == "VEVENT"
]
for component in event_components:
uid = component.get("uid")
uid = str(component.get("uid"))
recurrence_id = component.get(
"recurrence_id"
) # Unique identifier for a single event in a recurrence set
partner_ids = self._get_attendee_partners(component, current_user_email)
"recurrence-id"
) # Date & time a single event would normally be at in the series
existing_instance = self._get_existing_instance(uid, recurrence_id)
outdated = False
last_modified = component.get("last-modified") and component.decoded(
"last-modified"
)
if existing_instance and last_modified:
last_modified = last_modified.astimezone(utc).replace(tzinfo=None)
if last_modified < existing_instance.write_date:
# _logger.info(
# f"Last modified date {last_modified} is before most recent "
# f"write date {existing_instance.write_date}. Skipping."
# )
outdated = True
outdated = self._get_outdated(component, existing_instance, synced_events)
owned = (
existing_instance and existing_instance.partner_id == user.partner_id
)
values, recurrency_vals = (
self._get_vals_recurrency_vals_from_ical_component(
partner_ids, component, user
)
values = self._get_values_from_ical_component(component, user)
recurrency_vals = existing_instance._get_recurrency_values_from_ical_event(
component
)
changed_vals = {}
if not existing_instance:
_logger.info(f"Creating with vals: {values}")
self.with_context(caldav_no_sync=True).create(values)
_logger.info(f"Creating with vals: {values | recurrency_vals}")
new_event = self.with_context(caldav_no_sync=True).create(
values | recurrency_vals
)
self.env["calendar.event"].flush_model()
if new_event.recurrency:
synced_events |= new_event.recurrence_id.calendar_event_ids
else:
synced_events |= new_event
continue
elif outdated or not owned:
_logger.info(
f"Event {existing_instance.caldav_uid} "
f"{'outdated ' if outdated else ''}"
f"{'not owned by user ' + user.name if not owned else ''}."
f"Event {existing_instance.caldav_uid}-{existing_instance.caldav_recurrence_id} "
f"{'outdated' if outdated else ''}"
f"{'not owned by user' + user.name if not owned else ''}."
f" Skipping."
)
pass # Do nothing, it's not this user's event to modify or it's outdated
# Do nothing, it's not this user's event to modify or it's outdated
else:
# Don't update partner_ids if no change
if partner_ids != existing_instance.partner_ids:
changed_vals.update(partner_ids=values.pop("partner_ids"))
updated_attendees = existing_instance.attendee_ids.filtered(
lambda rec: rec.partner_id in partner_ids
changed_vals = existing_instance._get_recurrence_changes(
recurrency_vals
) | existing_instance._get_value_changes(values)
if changed_vals:
_logger.info(
f"Updating event {existing_instance.caldav_uid}-{existing_instance.caldav_recurrence_id} with : {changed_vals}"
)
changed_vals.update(
attendee_ids=[Command.set(updated_attendees.ids)]
)
else:
values.pop("partner_ids") # They break the equality check later
existing_instance.with_context(
caldav_no_sync=True,
).write(changed_vals)
if existing_instance.recurrency and existing_instance.is_base_event:
synced_events |= existing_instance.recurrence_id.calendar_event_ids
else:
synced_events |= existing_instance
return synced_events
# Get just the list of values that have changed, leave the others alone
for key, val in values.items():
curr_val = getattr(existing_instance, key)
# Can't deal with x2many fields, need ID from a record
if isinstance(val, list):
continue
if curr_val and isinstance(curr_val, models.Model):
if len(curr_val) > 1:
continue
curr_val = curr_val.id
if curr_val != val:
changed_vals.update({key: val})
self._update_event_recurrence(existing_instance, recurrency_vals)
_logger.info(f"Updating with : {changed_vals}")
existing_instance.with_context(
caldav_no_sync=True,
).write(changed_vals)
recurrence_id = str(component.get("recurrence-id"))
if recurrence_id == "None":
recurrence_id = ""
caldav_uids.add(f"{uid}{recurrence_id}")
return caldav_uids
@staticmethod
def _update_event_recurrence(existing_instance, recurrency_vals):
def _get_outdated(self, component, existing_instance, synced_events):
outdated = False
last_modified = component.get("last-modified")
if (
recurrency_vals
and recurrency_vals.get("recurrency")
and (
not existing_instance.recurrency
or not existing_instance.follow_recurrence
)
existing_instance
and last_modified
and existing_instance not in synced_events
):
existing_instance.write(
{
"recurrency": True,
"follow_recurrence": True,
}
)
last_modified = last_modified.dt.astimezone(utc).replace(tzinfo=None)
if last_modified < existing_instance.write_date:
outdated = True
return outdated
def _get_vals_recurrency_vals_from_ical_component(
self, attendee_ids, component, user
):
start = component.decoded("dtstart")
def _get_values_from_ical_component(self, component, user):
start = component.get("dtstart") and component.decoded("dtstart")
if isinstance(start, datetime):
start = start.astimezone(utc).replace(tzinfo=None)
end = component.decoded("dtend")
end = component.get("dtend") and component.decoded("dtend")
if isinstance(end, datetime):
end = end.astimezone(utc).replace(tzinfo=None)
organizer = self._get_organizer_partner(component)
attendee_ids = self._get_attendee_partners(component, user.partner_id.email)
values = {
"name": str(component.get("summary")),
"start": start,
@ -419,15 +551,12 @@ class CalendarEvent(models.Model):
"description": self._extract_component_text(component, "description"),
"location": self._extract_component_text(component, "location"),
"videocall_location": self._extract_component_text(component, "conference"),
"caldav_uid": component.get("uid"),
"caldav_uid": str(component.get("uid")),
"partner_ids": [(6, 0, attendee_ids.ids)],
"partner_id": organizer.id if organizer else False,
"partner_id": organizer.id if organizer else user.partner_id.id,
"user_id": user.id,
}
recurrency_vals = self._get_recurrency_values_from_ical_event(component)
if recurrency_vals:
values.update(recurrency_vals)
return values, recurrency_vals
return values
def _get_attendee_partners(self, component, current_user_email):
attendee_emails = self._get_ical_attendee_emails(component)
@ -450,7 +579,23 @@ class CalendarEvent(models.Model):
for email in missing_emails
]
)
return existing_partners | added_partners
final_attendees = {}
all_partners = existing_partners | added_partners
# We do this because partners may have identical emails and we only want one
# attending partner per email. Otherwise invitations get sent out multiple times
# and nobody likes that.
# Prioritize users as attendees
for partner in all_partners.filtered(lambda partner: bool(partner.user_id)):
if partner.email not in final_attendees:
final_attendees[partner.email] = partner.id
for partner in all_partners.filtered(lambda partner: not partner.user_id):
if partner.email not in final_attendees:
final_attendees[partner.email] = partner.id
return all_partners.filtered(
lambda partner: partner.id in final_attendees.values()
)
def _get_organizer_partner(self, component):
organizer = component.get("organizer")

View file

@ -0,0 +1,18 @@
from odoo import models, fields, api
from pytz import timezone
class RecurrenceRule(models.Model):
_inherit = "calendar.recurrence"
def _apply_recurrence(self, **kwargs):
detached_events = super()._apply_recurrence(**kwargs)
def _get_recurrence_id(event):
format = "%Y%m%d" if event.allday else "%Y%m%dT%H%M%S"
return event.start.astimezone(timezone(event.user_id.tz)).strftime(format)
for event in self.calendar_event_ids:
event.caldav_recurrence_id = _get_recurrence_id(event)
return detached_events