caldav_sync: v0.8.0 - disable notifications when polling server
- Disable sending of notification emails when events are created or updated in Odoo during a CalDAV server synchronization. - General code cleanup with improved type hints.
This commit is contained in:
parent
4b2b53caa7
commit
df5fc408de
6 changed files with 196 additions and 97 deletions
|
|
@ -54,8 +54,22 @@ Technical Details
|
||||||
Change Log
|
Change Log
|
||||||
----------
|
----------
|
||||||
|
|
||||||
17.0.0.6.0
|
0.8.0
|
||||||
^^^^^^^^^^
|
^^^^^
|
||||||
|
|
||||||
|
* Disable sending of notification emails when events are created or updated
|
||||||
|
in Odoo during a CalDAV server synchronization.
|
||||||
|
* General code cleanup with improved type hints.
|
||||||
|
|
||||||
|
0.7.0
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
* Stopped the import of past events when synchronizing from the CalDAV server.
|
||||||
|
This should help with performance, timeouts and avoid importing events that
|
||||||
|
are not relevant to the user.
|
||||||
|
|
||||||
|
0.6.0
|
||||||
|
^^^^^
|
||||||
|
|
||||||
* Fixed an issue where synchronizing events created duplicate events on every sync.
|
* Fixed an issue where synchronizing events created duplicate events on every sync.
|
||||||
* Completely revamped and synchronization of recurring events in both directions.
|
* Completely revamped and synchronization of recurring events in both directions.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "CalDAV Synchronization",
|
"name": "CalDAV Synchronization",
|
||||||
"version": "18.0.0.7.0",
|
"version": "18.0.0.8.0",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@
|
||||||
from . import calendar_event
|
from . import calendar_event
|
||||||
from . import res_users
|
from . import res_users
|
||||||
from . import calendar_recurrence
|
from . import calendar_recurrence
|
||||||
|
from . import calendar_attendee
|
||||||
|
|
|
||||||
24
caldav_sync/models/calendar_attendee.py
Normal file
24
caldav_sync/models/calendar_attendee.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
from odoo import models, api
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarAttendee(models.Model):
|
||||||
|
_inherit = "calendar.attendee"
|
||||||
|
|
||||||
|
def _send_mail_to_attendees(self, mail_template, force_send=False):
|
||||||
|
"""Override to prevent sending emails when dont_notify context is set.
|
||||||
|
|
||||||
|
:param mail_template: a mail.template record
|
||||||
|
:param force_send: if set to True, the mail(s) will be sent immediately (instead of the next queue processing)
|
||||||
|
:return: Result of super or False if notification is skipped
|
||||||
|
"""
|
||||||
|
# Check for dont_notify in context
|
||||||
|
if self.env.context.get("dont_notify"):
|
||||||
|
_logger.info("Email notifications skipped due to dont_notify context")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return super(CalendarAttendee, self)._send_mail_to_attendees(
|
||||||
|
mail_template, force_send
|
||||||
|
)
|
||||||
|
|
@ -3,20 +3,29 @@ import uuid
|
||||||
import icalendar.cal
|
import icalendar.cal
|
||||||
|
|
||||||
from odoo import models, api, fields, _
|
from odoo import models, api, fields, _
|
||||||
from odoo.tools.misc import _logger
|
|
||||||
from odoo.addons.calendar.models.calendar_recurrence import MAX_RECURRENT_EVENT
|
from odoo.addons.calendar.models.calendar_recurrence import MAX_RECURRENT_EVENT
|
||||||
import caldav
|
import caldav
|
||||||
|
from caldav.lib.error import NotFoundError
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from icalendar import vCalAddress, vText, vDatetime, vRecur, Event
|
from icalendar import vCalAddress, vText, vDatetime, vRecur, Event, vDate
|
||||||
import re
|
import re
|
||||||
from pytz import timezone, utc
|
from pytz import timezone, utc
|
||||||
from typing import List, Dict, TypeVar, Optional
|
from typing import List, Dict, Optional, Any, TYPE_CHECKING
|
||||||
from markdownify import markdownify as md
|
from markdownify import markdownify as md
|
||||||
import markdown2 as md2
|
import markdown2 as md2
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from odoo.addons.base.models.res_users import Users as User
|
||||||
|
from odoo.addons.base.models.res_partner import Partner
|
||||||
|
from odoo.addons.calendar.models.calendar_event import Meeting as OdooCalendarEvent
|
||||||
|
else:
|
||||||
|
User = models.Model
|
||||||
|
Partner = models.Model
|
||||||
|
OdooCalendarEvent = models.Model
|
||||||
|
|
||||||
WEEKDAY_MAP = {
|
WEEKDAY_MAP = {
|
||||||
0: "MO",
|
0: "MO",
|
||||||
1: "TU",
|
1: "TU",
|
||||||
|
|
@ -27,35 +36,59 @@ WEEKDAY_MAP = {
|
||||||
6: "SU",
|
6: "SU",
|
||||||
}
|
}
|
||||||
|
|
||||||
CalendarEvent = TypeVar("calendar.event", bound=models.Model)
|
|
||||||
User = TypeVar("res.users", bound=models.Model)
|
|
||||||
Partner = TypeVar("res.partner", bound=models.Model)
|
|
||||||
|
|
||||||
|
def _parse_rrule_string(rrule_str: str) -> Dict[str, Any]:
|
||||||
|
"""Parse a string representing an RRULE into a dictionary of its parts.
|
||||||
|
|
||||||
def _parse_rrule_string(rrule_str):
|
Takes a string like "RRULE:FREQ=WEEKLY;UNTIL=20221231T000000Z;BYDAY=MO"
|
||||||
def try_to_int(part):
|
and returns a dictionary with proper types for vRecur.
|
||||||
try:
|
"""
|
||||||
return int(part)
|
from icalendar import vDDDTypes, vWeekday, vFrequency
|
||||||
except Exception:
|
|
||||||
return part
|
|
||||||
|
|
||||||
regex_str = "RRULE:(.*)$"
|
def parse_value(key: str, value: str) -> Any:
|
||||||
regex = re.compile(regex_str)
|
if key == "UNTIL":
|
||||||
params_match = regex.search(rrule_str)
|
# Convert to datetime and wrap in vDDDTypes
|
||||||
params_part = params_match.groups()[0]
|
if "T" in value:
|
||||||
params = params_part.split(";")
|
dt = datetime.strptime(value, "%Y%m%dT%H%M%S")
|
||||||
params_dict = {}
|
|
||||||
for param in params:
|
|
||||||
parts = param.split("=")
|
|
||||||
if parts[0].upper() == "UNTIL":
|
|
||||||
if "T" in parts[1]:
|
|
||||||
parts[1] = datetime.strptime(parts[1], "%Y%m%dT%H%M%S")
|
|
||||||
else:
|
else:
|
||||||
parts[1] = datetime.strptime(parts[1], "%Y%m%d")
|
dt = datetime.strptime(value, "%Y%m%d")
|
||||||
if parts[0].upper() == "BYDAY":
|
return vDDDTypes(dt)
|
||||||
parts[1] = [part for part in parts[1].split(",")]
|
elif key in ("WKST", "BYDAY", "BYWEEKDAY"):
|
||||||
params_dict.update({parts[0]: try_to_int(parts[1])})
|
# Convert to vWeekday
|
||||||
return params_dict
|
return [vWeekday(day) for day in value.split(",")]
|
||||||
|
elif key == "FREQ":
|
||||||
|
# vFrequency will handle the conversion
|
||||||
|
return vFrequency(value)
|
||||||
|
elif key in (
|
||||||
|
"COUNT",
|
||||||
|
"INTERVAL",
|
||||||
|
"BYSECOND",
|
||||||
|
"BYMINUTE",
|
||||||
|
"BYHOUR",
|
||||||
|
"BYWEEKNO",
|
||||||
|
"BYMONTHDAY",
|
||||||
|
"BYYEARDAY",
|
||||||
|
"BYMONTH",
|
||||||
|
"BYSETPOS",
|
||||||
|
):
|
||||||
|
# Convert to int or list of ints
|
||||||
|
if "," in value:
|
||||||
|
return [int(v) for v in value.split(",")]
|
||||||
|
return int(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
if not rrule_str.startswith("RRULE:"):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
params = rrule_str[6:] # Remove 'RRULE:'
|
||||||
|
result = {}
|
||||||
|
for param in params.split(";"):
|
||||||
|
if "=" in param:
|
||||||
|
key, value = param.split("=", 1)
|
||||||
|
key = key.upper()
|
||||||
|
result[key] = parse_value(key, value)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _extract_vcal_email(vcal_address):
|
def _extract_vcal_email(vcal_address):
|
||||||
|
|
@ -134,19 +167,6 @@ class CalendarEvent(models.Model):
|
||||||
or not event.follow_recurrence
|
or not event.follow_recurrence
|
||||||
)
|
)
|
||||||
|
|
||||||
@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", "recurrence_id.base_event_id")
|
@api.depends("recurrency", "recurrence_id", "recurrence_id.base_event_id")
|
||||||
def _compute_is_base_event(self):
|
def _compute_is_base_event(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -196,7 +216,9 @@ class CalendarEvent(models.Model):
|
||||||
try:
|
try:
|
||||||
caldav_events = event._create_in_icalendar(calendar)
|
caldav_events = event._create_in_icalendar(calendar)
|
||||||
for caldav_event in caldav_events:
|
for caldav_event in caldav_events:
|
||||||
caldav_uid = caldav_event.vobject_instance.vevent.uid.value
|
caldav_uid = (
|
||||||
|
caldav_event.vobject_instance.vevent.uid.value
|
||||||
|
) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
event.with_context(caldav_no_sync=True).write(
|
event.with_context(caldav_no_sync=True).write(
|
||||||
{"caldav_uid": caldav_uid}
|
{"caldav_uid": caldav_uid}
|
||||||
)
|
)
|
||||||
|
|
@ -224,6 +246,11 @@ class CalendarEvent(models.Model):
|
||||||
calendar = client.calendar(url=user.caldav_calendar_url)
|
calendar = client.calendar(url=user.caldav_calendar_url)
|
||||||
|
|
||||||
base_event = self._get_caldav_base_event_by_uid(calendar, self.caldav_uid)
|
base_event = self._get_caldav_base_event_by_uid(calendar, self.caldav_uid)
|
||||||
|
if not base_event:
|
||||||
|
_logger.warning(
|
||||||
|
f"Failed to find base event for {self} on CalDAV server."
|
||||||
|
)
|
||||||
|
return
|
||||||
if self.recurrence_id:
|
if self.recurrence_id:
|
||||||
tz = timezone(self.event_tz or self.env.user.tz)
|
tz = timezone(self.event_tz or self.env.user.tz)
|
||||||
start = utc.localize(self.start).astimezone(tz)
|
start = utc.localize(self.start).astimezone(tz)
|
||||||
|
|
@ -260,12 +287,14 @@ class CalendarEvent(models.Model):
|
||||||
|
|
||||||
def _update_base_caldav_event(
|
def _update_base_caldav_event(
|
||||||
self,
|
self,
|
||||||
calendar: icalendar.cal.Calendar,
|
calendar: caldav.Calendar,
|
||||||
event: caldav.Event,
|
event: caldav.Event,
|
||||||
ical_event_data: dict,
|
ical_event_data: dict,
|
||||||
):
|
):
|
||||||
if event:
|
if event:
|
||||||
self._update_ical_event_values(event.icalendar_component, ical_event_data)
|
self._update_ical_event_values(
|
||||||
|
event.icalendar_component, ical_event_data
|
||||||
|
) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
event.save()
|
event.save()
|
||||||
else:
|
else:
|
||||||
calendar.add_event(**ical_event_data)
|
calendar.add_event(**ical_event_data)
|
||||||
|
|
@ -281,9 +310,11 @@ class CalendarEvent(models.Model):
|
||||||
|
|
||||||
def _get_caldav_base_event_by_uid(
|
def _get_caldav_base_event_by_uid(
|
||||||
self, calendar: caldav.Calendar, uid: str
|
self, calendar: caldav.Calendar, uid: str
|
||||||
) -> Optional[CalendarEvent]:
|
) -> Optional[caldav.Event]:
|
||||||
for event in calendar.events():
|
for event in calendar.events():
|
||||||
component = event.icalendar_component
|
component = (
|
||||||
|
event.icalendar_component
|
||||||
|
) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
event_uid = self._extract_component_text(component, "uid")
|
event_uid = self._extract_component_text(component, "uid")
|
||||||
if event_uid == uid and not component.get("recurrence-id"):
|
if event_uid == uid and not component.get("recurrence-id"):
|
||||||
return event
|
return event
|
||||||
|
|
@ -316,6 +347,7 @@ class CalendarEvent(models.Model):
|
||||||
calendar = client.calendar(url=user.caldav_calendar_url)
|
calendar = client.calendar(url=user.caldav_calendar_url)
|
||||||
try:
|
try:
|
||||||
caldav_event = calendar.event_by_uid(self.caldav_uid)
|
caldav_event = calendar.event_by_uid(self.caldav_uid)
|
||||||
|
assert isinstance(caldav_event, caldav.Event)
|
||||||
if not delete_all and self.recurrence_id and not self.is_base_event:
|
if not delete_all and self.recurrence_id and not self.is_base_event:
|
||||||
index = self._get_subcomponent_index_for_recurrence(
|
index = self._get_subcomponent_index_for_recurrence(
|
||||||
caldav_event
|
caldav_event
|
||||||
|
|
@ -330,14 +362,16 @@ class CalendarEvent(models.Model):
|
||||||
# of the event matches.
|
# of the event matches.
|
||||||
if delete_all or self._matches_caldav_start(caldav_event):
|
if delete_all or self._matches_caldav_start(caldav_event):
|
||||||
caldav_event.delete()
|
caldav_event.delete()
|
||||||
except caldav.error.NotFoundError:
|
except NotFoundError:
|
||||||
# No worries - it just didn't exist so nothing to sync
|
# No worries - it just didn't exist so nothing to sync
|
||||||
pass
|
pass
|
||||||
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 _matches_caldav_start(self, caldav_event: caldav.Event) -> bool:
|
def _matches_caldav_start(self, caldav_event: caldav.Event) -> bool:
|
||||||
event_start = caldav_event.icalendar_component.get("dtstart").dt
|
event_start = caldav_event.icalendar_component.get(
|
||||||
|
"dtstart"
|
||||||
|
).dt # pyright: ignore[reportAttributeAccessIssue]
|
||||||
tz = event_start.tzinfo
|
tz = event_start.tzinfo
|
||||||
self_start = utc.localize(self.start).astimezone(tz)
|
self_start = utc.localize(self.start).astimezone(tz)
|
||||||
return self_start == event_start
|
return self_start == event_start
|
||||||
|
|
@ -464,7 +498,6 @@ class CalendarEvent(models.Model):
|
||||||
)
|
)
|
||||||
event_data["dtstart"] = vDatetime(utc.localize(self.start).astimezone(event_tz))
|
event_data["dtstart"] = vDatetime(utc.localize(self.start).astimezone(event_tz))
|
||||||
event_data["dtend"] = vDatetime(utc.localize(self.stop).astimezone(event_tz))
|
event_data["dtend"] = vDatetime(utc.localize(self.stop).astimezone(event_tz))
|
||||||
return event_data
|
|
||||||
|
|
||||||
def _add_event_recurrence_id(self, event_data: Dict) -> None:
|
def _add_event_recurrence_id(self, event_data: Dict) -> None:
|
||||||
"""Add the recurrence-id parameter to event data if self is linked
|
"""Add the recurrence-id parameter to event data if self is linked
|
||||||
|
|
@ -532,7 +565,8 @@ class CalendarEvent(models.Model):
|
||||||
synchronize them with their Odoo calendar."""
|
synchronize them with their Odoo calendar."""
|
||||||
all_users = self.env["res.users"].search([("is_caldav_enabled", "=", True)])
|
all_users = self.env["res.users"].search([("is_caldav_enabled", "=", True)])
|
||||||
for user in all_users:
|
for user in all_users:
|
||||||
self._poll_user_caldav_server(user)
|
self.with_context(dont_notify=True)._poll_user_caldav_server(user)
|
||||||
|
# self._poll_user_caldav_server(user)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _poll_user_caldav_server(self, user) -> None:
|
def _poll_user_caldav_server(self, user) -> None:
|
||||||
|
|
@ -569,7 +603,7 @@ class CalendarEvent(models.Model):
|
||||||
# There are some events remaining in this recurrence series,
|
# There are some events remaining in this recurrence series,
|
||||||
# so we have synchronized them individually.
|
# so we have synchronized them individually.
|
||||||
pass
|
pass
|
||||||
except caldav.error.NotFoundError:
|
except NotFoundError:
|
||||||
# There are no more events with this UID, so we need to clear
|
# There are no more events with this UID, so we need to clear
|
||||||
# out the whole recurrence chain from the Odoo side.
|
# out the whole recurrence chain from the Odoo side.
|
||||||
ctx = {"caldav_no_sync": True}
|
ctx = {"caldav_no_sync": True}
|
||||||
|
|
@ -583,9 +617,8 @@ class CalendarEvent(models.Model):
|
||||||
).with_user(user).unlink()
|
).with_user(user).unlink()
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _sync_event_from_ical(
|
@api.returns("calendar.event")
|
||||||
self, ical_event: icalendar.cal.Event, user: User
|
def _sync_event_from_ical(self, ical_event: icalendar.cal.Event, user: User):
|
||||||
) -> CalendarEvent:
|
|
||||||
"""Given an iCalendar event, compare the event with any existing
|
"""Given an iCalendar event, compare the event with any existing
|
||||||
Odoo event that it matches and synchronize the changes. If no event
|
Odoo event that it matches and synchronize the changes. If no event
|
||||||
exists, create one iff the event is in the future.
|
exists, create one iff the event is in the future.
|
||||||
|
|
@ -609,14 +642,17 @@ class CalendarEvent(models.Model):
|
||||||
existing_instance = self._get_existing_instance(uid, recurrence_id)
|
existing_instance = self._get_existing_instance(uid, recurrence_id)
|
||||||
outdated = self._get_outdated(component, existing_instance, synced_events)
|
outdated = self._get_outdated(component, existing_instance, synced_events)
|
||||||
owned = (
|
owned = (
|
||||||
existing_instance and existing_instance.partner_id == user.partner_id
|
existing_instance and existing_instance.partner_id == user.partner_id
|
||||||
)
|
)
|
||||||
# Pass for_creation=True only when creating a new event
|
# Pass for_creation=True only when creating a new event
|
||||||
values = self._get_values_from_ical_component(component, user, for_creation=not existing_instance)
|
values = self._get_values_from_ical_component(
|
||||||
|
component, user, for_creation=not existing_instance
|
||||||
|
)
|
||||||
recurrency_vals = self._get_recurrency_values_from_ical_event(component)
|
recurrency_vals = self._get_recurrency_values_from_ical_event(component)
|
||||||
if not existing_instance:
|
if not existing_instance:
|
||||||
# If the event is in the past, we just ignore it.
|
# If the event is in the past, we just ignore it.
|
||||||
if values.get("stop") < datetime.now(tz=None):
|
stop = values.get("stop")
|
||||||
|
if stop and stop < datetime.now(tz=None):
|
||||||
continue
|
continue
|
||||||
# If we're creating an instance and it doesn't follow the recurrence,
|
# If we're creating an instance and it doesn't follow the recurrence,
|
||||||
# just scrap the recurrency vals, they're not useful
|
# just scrap the recurrency vals, they're not useful
|
||||||
|
|
@ -651,7 +687,8 @@ class CalendarEvent(models.Model):
|
||||||
return synced_events
|
return synced_events
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_existing_instance(self, uid, recurrence_id: Optional[datetime]) -> CalendarEvent:
|
@api.returns("calendar.event")
|
||||||
|
def _get_existing_instance(self, uid, recurrence_id: Optional[datetime]):
|
||||||
"""Find the Odoo calendar.event record matching uid and,
|
"""Find the Odoo calendar.event record matching uid and,
|
||||||
if set, recurrence_id.
|
if set, recurrence_id.
|
||||||
"""
|
"""
|
||||||
|
|
@ -723,14 +760,20 @@ class CalendarEvent(models.Model):
|
||||||
"recurrence_update": "self_only",
|
"recurrence_update": "self_only",
|
||||||
}
|
}
|
||||||
|
|
||||||
if not rrule:
|
if not rrule or not isinstance(rrule, vRecur):
|
||||||
return {}
|
return {}
|
||||||
if rrule.get("until"):
|
until = rrule.get("until")
|
||||||
rrule["until"] = rrule.get("until")[0].astimezone(utc)
|
if until and isinstance(until, list):
|
||||||
|
until = until[0].astimezone(utc)
|
||||||
rrule_str = rrule.to_ical() and rrule.to_ical().decode("utf-8")
|
rrule_str = rrule.to_ical() and rrule.to_ical().decode("utf-8")
|
||||||
rrule_params = self.env["calendar.recurrence"]._rrule_parse(
|
if rrule_str:
|
||||||
"RRULE:" + rrule_str, component.get("dtstart").dt.astimezone(utc)
|
rrule_params = self.env["calendar.recurrence"]._rrule_parse(
|
||||||
)
|
"RRULE:" + rrule_str, component.get("dtstart").dt.astimezone(utc)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_logger.warning(f"Could not convert RRULE to string: {rrule}")
|
||||||
|
return {}
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
"recurrency": True,
|
"recurrency": True,
|
||||||
"follow_recurrence": True,
|
"follow_recurrence": True,
|
||||||
|
|
@ -748,8 +791,9 @@ class CalendarEvent(models.Model):
|
||||||
vals.update(end_type="count")
|
vals.update(end_type="count")
|
||||||
if not vals.get("count"):
|
if not vals.get("count"):
|
||||||
vals.update(count=MAX_RECURRENT_EVENT)
|
vals.update(count=MAX_RECURRENT_EVENT)
|
||||||
if vals.get("until"):
|
until = vals.get("until")
|
||||||
until_day = vals.get("until").date()
|
if until and (isinstance(until, vDatetime) or isinstance(until, vDate)):
|
||||||
|
until_day = until.dt if isinstance(until, vDatetime) else until.dt
|
||||||
vals.update(until=until_day)
|
vals.update(until=until_day)
|
||||||
vals.pop("count", None)
|
vals.pop("count", None)
|
||||||
return vals
|
return vals
|
||||||
|
|
@ -818,8 +862,8 @@ class CalendarEvent(models.Model):
|
||||||
def _get_outdated(
|
def _get_outdated(
|
||||||
self,
|
self,
|
||||||
component: icalendar.cal.Component,
|
component: icalendar.cal.Component,
|
||||||
existing_instance: CalendarEvent,
|
existing_instance: OdooCalendarEvent,
|
||||||
synced_events: CalendarEvent,
|
synced_events: OdooCalendarEvent,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check whether a component from the CalDAV server (typically an
|
"""Check whether a component from the CalDAV server (typically an
|
||||||
event) is outdated when compared to its existing Odoo calendar.event
|
event) is outdated when compared to its existing Odoo calendar.event
|
||||||
|
|
@ -884,17 +928,25 @@ class CalendarEvent(models.Model):
|
||||||
organizer_partner = self._get_organizer_partner(component)
|
organizer_partner = self._get_organizer_partner(component)
|
||||||
if organizer_partner:
|
if organizer_partner:
|
||||||
# Get the Odoo user ID associated with the organizer partner
|
# Get the Odoo user ID associated with the organizer partner
|
||||||
organizer = organizer_partner.user_ids[0].id if organizer_partner.user_ids else False
|
organizer = (
|
||||||
values.update({
|
organizer_partner.user_ids[0].id
|
||||||
"partner_id": organizer_partner.id,
|
if organizer_partner.user_ids
|
||||||
"user_id": organizer,
|
else False
|
||||||
})
|
)
|
||||||
|
values.update(
|
||||||
|
{
|
||||||
|
"partner_id": organizer_partner.id,
|
||||||
|
"user_id": organizer,
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# For new events without an organizer, use the current user
|
# For new events without an organizer, use the current user
|
||||||
values.update({
|
values.update(
|
||||||
"partner_id": user.partner_id.id,
|
{
|
||||||
"user_id": user.id,
|
"partner_id": user.partner_id.id,
|
||||||
})
|
"user_id": user.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,10 @@ def _get_ics_path(filename):
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None, futurize=True):
|
def _patch_caldav_with_events_from_ics(
|
||||||
with (
|
ics_paths, user, last_modified=None, futurize=True
|
||||||
patch("caldav.DAVClient") as MockDAVClient,
|
):
|
||||||
patch("caldav.Calendar") as MockCalendar,
|
with patch("caldav.DAVClient") as MockDAVClient:
|
||||||
):
|
|
||||||
mock_client = MockDAVClient.return_value
|
mock_client = MockDAVClient.return_value
|
||||||
mock_calendars = {}
|
mock_calendars = {}
|
||||||
|
|
||||||
|
|
@ -70,14 +69,20 @@ def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None, futu
|
||||||
for event in ical_events:
|
for event in ical_events:
|
||||||
for subcomponent in event.subcomponents:
|
for subcomponent in event.subcomponents:
|
||||||
if subcomponent.name == "VEVENT":
|
if subcomponent.name == "VEVENT":
|
||||||
start = subcomponent.get("dtstart") and subcomponent.decoded("dtstart")
|
start = subcomponent.get("dtstart") and subcomponent.decoded(
|
||||||
end = subcomponent.get("dtend") and subcomponent.decoded("dtend")
|
"dtstart"
|
||||||
|
)
|
||||||
|
end = subcomponent.get("dtend") and subcomponent.decoded(
|
||||||
|
"dtend"
|
||||||
|
)
|
||||||
if isinstance(start, datetime) and isinstance(end, datetime):
|
if isinstance(start, datetime) and isinstance(end, datetime):
|
||||||
duration = end - start
|
duration = end - start
|
||||||
else:
|
else:
|
||||||
duration = timedelta(hours=1)
|
duration = timedelta(hours=1)
|
||||||
subcomponent["dtstart"] = icalendar.vDDDTypes(datetime.now())
|
subcomponent["dtstart"] = icalendar.vDDDTypes(datetime.now())
|
||||||
subcomponent["dtend"] = icalendar.vDDDTypes(datetime.now() + duration)
|
subcomponent["dtend"] = icalendar.vDDDTypes(
|
||||||
|
datetime.now() + duration
|
||||||
|
)
|
||||||
|
|
||||||
base_events = [event for event in ical_events if not event.get("recurrence-id")]
|
base_events = [event for event in ical_events if not event.get("recurrence-id")]
|
||||||
for base_event in base_events:
|
for base_event in base_events:
|
||||||
|
|
@ -97,7 +102,7 @@ def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None, futu
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@tagged('post_install', '-at_install')
|
@tagged("post_install", "-at_install")
|
||||||
class TestCalendarEvent(TransactionCase, CaldavTestCommon):
|
class TestCalendarEvent(TransactionCase, CaldavTestCommon):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
|
@ -261,7 +266,6 @@ class TestCalendarEvent(TransactionCase, CaldavTestCommon):
|
||||||
self.assertIn(user2.partner_id, event.partner_ids)
|
self.assertIn(user2.partner_id, event.partner_ids)
|
||||||
self.assertIn(user3.partner_id, event.partner_ids)
|
self.assertIn(user3.partner_id, event.partner_ids)
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_user_attendees_event_from_server_update(self):
|
def test_multiple_user_attendees_event_from_server_update(self):
|
||||||
"""Test event has (as in above test):
|
"""Test event has (as in above test):
|
||||||
Organizer: user1 (test1@example.com)
|
Organizer: user1 (test1@example.com)
|
||||||
|
|
@ -277,21 +281,25 @@ class TestCalendarEvent(TransactionCase, CaldavTestCommon):
|
||||||
self.env["calendar.event"].poll_caldav_server()
|
self.env["calendar.event"].poll_caldav_server()
|
||||||
with _patch_caldav_with_events_from_ics(ics_path, user3):
|
with _patch_caldav_with_events_from_ics(ics_path, user3):
|
||||||
self.env["calendar.event"].poll_caldav_server()
|
self.env["calendar.event"].poll_caldav_server()
|
||||||
|
notification_method = "odoo.addons.calendar.models.calendar_attendee.Attendee._send_mail_to_attendees"
|
||||||
# Now update it to remove one attendee
|
# Now update it to remove one attendee
|
||||||
# Shuffle the user polling order just to test more robustly
|
# Shuffle the user polling order just to test more robustly
|
||||||
ics_path = _get_ics_path("test_multi_user_update.ics")
|
ics_path = _get_ics_path("test_multi_user_update.ics")
|
||||||
with _patch_caldav_with_events_from_ics(
|
with _patch_caldav_with_events_from_ics(
|
||||||
ics_path, user2, last_modified=datetime.now(UTC)
|
ics_path, user2, last_modified=datetime.now(UTC)
|
||||||
):
|
), patch(notification_method) as mock_notification_method:
|
||||||
self.env["calendar.event"].poll_caldav_server()
|
self.env["calendar.event"].poll_caldav_server()
|
||||||
|
mock_notification_method.assert_not_called()
|
||||||
with _patch_caldav_with_events_from_ics(
|
with _patch_caldav_with_events_from_ics(
|
||||||
ics_path, user3, last_modified=datetime.now(UTC)
|
ics_path, user3, last_modified=datetime.now(UTC)
|
||||||
):
|
), patch(notification_method) as mock_notification_method:
|
||||||
self.env["calendar.event"].poll_caldav_server()
|
self.env["calendar.event"].poll_caldav_server()
|
||||||
|
mock_notification_method.assert_not_called()
|
||||||
with _patch_caldav_with_events_from_ics(
|
with _patch_caldav_with_events_from_ics(
|
||||||
ics_path, user1, last_modified=datetime.now(UTC)
|
ics_path, user1, last_modified=datetime.now(UTC)
|
||||||
):
|
), patch(notification_method) as mock_notification_method:
|
||||||
self.env["calendar.event"].poll_caldav_server()
|
self.env["calendar.event"].poll_caldav_server()
|
||||||
|
mock_notification_method.assert_not_called()
|
||||||
event = self.env["calendar.event"].search(
|
event = self.env["calendar.event"].search(
|
||||||
[("caldav_uid", "=", "2495546B-5C9A-4632-AAD3-A179EF83CF20")]
|
[("caldav_uid", "=", "2495546B-5C9A-4632-AAD3-A179EF83CF20")]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue