* Fixed an issue where accepting events from an external organizer would send emails to the all event attendees upon synchronization. * Corrected the data model for events where multiple Odoo users are attendees for the same event. Now, only one event is created in Odoo, with all the attendees on the same event. This conforms to Odoo's existing calendar event data model. * Updated the setting of the organizer field (user_id) on the calendar events upon synchronization. Previously, the organizer field was always set to the current user whose events were being synchronized. Now, it is set based on the ORGANIZER parameter of the iCalendar event, if present. If not present, it defaults to the current synchronizing user. In the case that the organizer is external, the user_id field is left blank.
305 lines
13 KiB
Python
305 lines
13 KiB
Python
from collections.abc import Iterable
|
|
from odoo.tests import TransactionCase
|
|
from odoo import Command
|
|
from unittest.mock import patch, MagicMock, DEFAULT
|
|
import icalendar
|
|
from pathlib import Path
|
|
from .common import CaldavTestCommon
|
|
from contextlib import contextmanager
|
|
from datetime import datetime, UTC, timedelta
|
|
import caldav
|
|
|
|
WEEKDAY_MAP = {
|
|
0: "SUN",
|
|
1: "MON",
|
|
2: "TUE",
|
|
3: "WED",
|
|
4: "THU",
|
|
5: "FRI",
|
|
6: "SAT",
|
|
}
|
|
|
|
|
|
def _get_ics_path(filename):
|
|
return Path(__file__).parent / "data" / filename
|
|
|
|
|
|
@contextmanager
|
|
def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None):
|
|
with (
|
|
patch("caldav.DAVClient") as MockDAVClient,
|
|
patch("caldav.Calendar") as MockCalendar,
|
|
):
|
|
mock_client = MockDAVClient.return_value
|
|
mock_calendars = {}
|
|
|
|
def calendar_side_effect(url):
|
|
if url not in mock_calendars:
|
|
mock_cal = MagicMock()
|
|
mock_cal.events = MagicMock(return_value=[])
|
|
mock_cal.event_by_uid = MagicMock()
|
|
mock_calendars[url] = mock_cal
|
|
return mock_calendars[url]
|
|
|
|
mock_client.calendar = calendar_side_effect
|
|
|
|
# Get or create the mock calendar for this user
|
|
mock_calendar = calendar_side_effect(user.caldav_calendar_url)
|
|
|
|
def event_by_uid_side_effect(self, uid):
|
|
for event in self.events():
|
|
if str(event.icalendar_component.get("uid")) == uid:
|
|
return event
|
|
return DEFAULT
|
|
|
|
ical_events = []
|
|
if ics_paths:
|
|
if not isinstance(ics_paths, Iterable):
|
|
ics_paths = [ics_paths] if ics_paths else []
|
|
for ics_path in ics_paths:
|
|
with ics_path.open("rb") as file:
|
|
ical_content = file.read()
|
|
ical_events.append(icalendar.Calendar.from_ical(ical_content))
|
|
if last_modified:
|
|
for event in ical_events:
|
|
for subcomponent in event.subcomponents:
|
|
if subcomponent.name == "VEVENT":
|
|
subcomponent["last-modified"] = icalendar.vDate(last_modified)
|
|
subcomponent["dtstamp"] = icalendar.vDate(last_modified)
|
|
|
|
base_events = [event for event in ical_events if not event.get("recurrence-id")]
|
|
for base_event in base_events:
|
|
child_events = [
|
|
event
|
|
for event in ical_events
|
|
if event.get("recurrence-id")
|
|
and event.get("uid") == base_event.get("uid")
|
|
]
|
|
for child_event in child_events:
|
|
base_event.add_component(child_event)
|
|
mock_calendar.add_event(base_event)
|
|
caldav_events = [caldav.Event(data=event) for event in base_events]
|
|
mock_calendar.events.return_value = caldav_events
|
|
mock_calendar.event_by_uid.side_effect = event_by_uid_side_effect
|
|
user._compute_is_caldav_enabled()
|
|
yield
|
|
|
|
|
|
class TestCalendarEvent(TransactionCase, CaldavTestCommon):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.env["res.users"].search([])._compute_is_caldav_enabled()
|
|
cls.user_1_url = "https://mycaldav.test.com/test1calendar"
|
|
cls.user_1 = cls._generate_user(
|
|
"test1",
|
|
caldav_username="user1",
|
|
caldav_password="pass1",
|
|
caldav_url=cls.user_1_url,
|
|
)
|
|
cls.user_2_url = "https://mycaldav.test.com/test2calendar"
|
|
cls.user_2 = cls._generate_user(
|
|
"test2",
|
|
caldav_username="user2",
|
|
caldav_password="pass2",
|
|
caldav_url=cls.user_2_url,
|
|
)
|
|
cls.user_3_url = "https://mycaldav.test.com/test3calendar"
|
|
cls.user_3 = cls._generate_user(
|
|
"test3",
|
|
caldav_username="user3",
|
|
caldav_password="pass3",
|
|
caldav_url=cls.user_3_url,
|
|
)
|
|
|
|
def test_basic_event_from_server_create(self):
|
|
user = self.user_1
|
|
ics_path = _get_ics_path("basic.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user):
|
|
current_events = self.env["calendar.event"].search([])
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
events_after_sync = self.env["calendar.event"].search([])
|
|
new_events = events_after_sync - current_events
|
|
self.assertEqual(len(new_events), 1)
|
|
|
|
def test_basic_event_from_server_update(self):
|
|
user = self.user_1
|
|
ics_path = _get_ics_path("basic.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
|
orig_start = event.start
|
|
orig_stop = event.stop
|
|
ics_path = _get_ics_path("basic_updated.ics")
|
|
with _patch_caldav_with_events_from_ics(
|
|
ics_path,
|
|
user,
|
|
last_modified=(datetime.now(UTC)),
|
|
):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
|
self.assertEqual(event.name, "Test Updated")
|
|
# This next one is just lazy avoiding the HTML stripping
|
|
self.assertIn("Some note ...", event.description)
|
|
self.assertGreater(event.start, orig_start)
|
|
self.assertGreater(event.stop, orig_stop)
|
|
|
|
def test_basic_event_from_server_delete(self):
|
|
user = self.user_1
|
|
ics_path = _get_ics_path("basic.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
# Passing None to ics_path means no events returned from server
|
|
with _patch_caldav_with_events_from_ics(None, user):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
|
self.assertFalse(event)
|
|
|
|
def test_recurring_from_server_create(self):
|
|
user = self.user_1
|
|
ics_path = _get_ics_path("test_recurring.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
events = self.env["calendar.event"].search(
|
|
[("partner_id", "=", user.partner_id.id)]
|
|
)
|
|
self.assertEqual(len(events), 10)
|
|
|
|
def test_multiple_attendees_event_from_server_create(self):
|
|
user = self.user_1
|
|
ics_path = _get_ics_path("test_multi_attendee.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
|
self.assertEqual(len(event.attendee_ids), 3)
|
|
self.assertIn(user.partner_id, event.attendee_ids.partner_id)
|
|
|
|
def test_multiple_attendees_event_from_server_update(self):
|
|
user = self.user_1
|
|
ics_path = _get_ics_path("test_multi_attendee.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
|
ics_path = _get_ics_path("test_multi_attendee_update.ics")
|
|
with _patch_caldav_with_events_from_ics(
|
|
ics_path, user, last_modified=datetime.now(UTC)
|
|
):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
self.assertEqual(len(event.attendee_ids), 2)
|
|
self.assertIn(user.partner_id, event.attendee_ids.partner_id)
|
|
|
|
def test_multiple_attendees_event_from_server_delete(self):
|
|
user = self.user_1
|
|
ics_path = _get_ics_path("test_multi_attendee.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
# Passing None as ics_path means no events returned from server
|
|
with _patch_caldav_with_events_from_ics(None, user):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
|
self.assertFalse(event)
|
|
|
|
def test_multiple_user_attendees_event_from_server_create(self):
|
|
"""Test event has:
|
|
Organizer: user1 (test1@example.com)
|
|
Attendees: user2 and user3 (test2@example.com, test3@example.com)
|
|
"""
|
|
user1 = self.user_1
|
|
user2 = self.user_2
|
|
user3 = self.user_3
|
|
ics_path = _get_ics_path("test_multi_user.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user1):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
with _patch_caldav_with_events_from_ics(ics_path, user2):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
with _patch_caldav_with_events_from_ics(ics_path, user3):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
event = self.env["calendar.event"].search(
|
|
[("caldav_uid", "=", "2495546B-5C9A-4632-AAD3-A179EF83CF20")]
|
|
)
|
|
self.assertEqual(len(event), 1)
|
|
# Make sure the event wasn't duplicated all over the place
|
|
other_user_events = self.env["calendar.event"].search(
|
|
[("user_id", "in", [user2.id, user3.id])]
|
|
)
|
|
self.assertFalse(other_user_events)
|
|
self.assertIn(user2.partner_id, event.partner_ids)
|
|
self.assertIn(user3.partner_id, event.partner_ids)
|
|
|
|
def test_multiple_user_attendees_event_from_server_update(self):
|
|
"""Test event has (as in above test):
|
|
Organizer: user1 (test1@example.com)
|
|
Attendees: user2 and user3 (test2@example.com, test3@example.com)
|
|
"""
|
|
user1 = self.user_1
|
|
user2 = self.user_2
|
|
user3 = self.user_3
|
|
ics_path = _get_ics_path("test_multi_user.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user1):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
with _patch_caldav_with_events_from_ics(ics_path, user2):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
with _patch_caldav_with_events_from_ics(ics_path, user3):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
# Now update it to remove one attendee
|
|
# Shuffle the user polling order just to test more robustly
|
|
ics_path = _get_ics_path("test_multi_user_update.ics")
|
|
with _patch_caldav_with_events_from_ics(
|
|
ics_path, user2, last_modified=datetime.now(UTC)
|
|
):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
with _patch_caldav_with_events_from_ics(
|
|
ics_path, user3, last_modified=datetime.now(UTC)
|
|
):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
with _patch_caldav_with_events_from_ics(
|
|
ics_path, user1, last_modified=datetime.now(UTC)
|
|
):
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
event = self.env["calendar.event"].search(
|
|
[("caldav_uid", "=", "2495546B-5C9A-4632-AAD3-A179EF83CF20")]
|
|
)
|
|
self.assertIn(user3.partner_id, event.partner_ids)
|
|
self.assertNotIn(user2.partner_id, event.partner_ids)
|
|
self.assertEqual(len(event.attendee_ids), 2)
|
|
|
|
def _create_multi_user_test_event(self):
|
|
return (
|
|
self.env["calendar.event"]
|
|
.with_user(self.user_1)
|
|
.create(
|
|
{
|
|
"name": "Test event",
|
|
"partner_ids": [
|
|
Command.set(
|
|
[
|
|
self.user_2.partner_id.id,
|
|
self.user_3.partner_id.id,
|
|
self.user_1.partner_id.id,
|
|
]
|
|
),
|
|
Command.create(
|
|
{
|
|
"name": "Test partner",
|
|
"email": "testpartner@example.com",
|
|
}
|
|
),
|
|
],
|
|
"start": datetime.now() + timedelta(days=2),
|
|
"stop": datetime.now() + timedelta(days=2, hours=1),
|
|
}
|
|
)
|
|
)
|
|
|
|
@contextmanager
|
|
def _patch_all_3_users_davclients(self):
|
|
with patch("caldav.DAVClient") as MockDAVClient:
|
|
(self.user_1 | self.user_2 | self.user_3)._compute_is_caldav_enabled()
|
|
|
|
mock_client = MockDAVClient.return_value
|
|
mock_calendar = MagicMock()
|
|
mock_event_by_uid = MagicMock()
|
|
mock_client.calendar.return_value = mock_calendar
|
|
mock_calendar.events.return_value = []
|
|
mock_calendar.event_by_uid.return_value = mock_event_by_uid
|
|
yield mock_client, mock_calendar
|