- 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.
349 lines
15 KiB
Python
349 lines
15 KiB
Python
from collections.abc import Iterable
|
|
from odoo.tests import TransactionCase, tagged
|
|
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, futurize=True
|
|
):
|
|
with patch("caldav.DAVClient") as MockDAVClient:
|
|
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(uid):
|
|
for event in mock_calendar.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)
|
|
if futurize:
|
|
for event in ical_events:
|
|
for subcomponent in event.subcomponents:
|
|
if subcomponent.name == "VEVENT":
|
|
start = subcomponent.get("dtstart") and subcomponent.decoded(
|
|
"dtstart"
|
|
)
|
|
end = subcomponent.get("dtend") and subcomponent.decoded(
|
|
"dtend"
|
|
)
|
|
if isinstance(start, datetime) and isinstance(end, datetime):
|
|
duration = end - start
|
|
else:
|
|
duration = timedelta(hours=1)
|
|
subcomponent["dtstart"] = icalendar.vDDDTypes(datetime.now())
|
|
subcomponent["dtend"] = icalendar.vDDDTypes(
|
|
datetime.now() + duration
|
|
)
|
|
|
|
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
|
|
|
|
|
|
@tagged("post_install", "-at_install")
|
|
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_past_event_from_server_no_create(self):
|
|
user = self.user_1
|
|
ics_path = _get_ics_path("basic.ics")
|
|
with _patch_caldav_with_events_from_ics(ics_path, user, futurize=False):
|
|
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), 0)
|
|
|
|
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()
|
|
|
|
# Verify the event was created correctly
|
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
|
self.assertEqual(event.name, "Test")
|
|
orig_start = event.start
|
|
orig_stop = event.stop
|
|
|
|
# Now update the event with the updated ICS data
|
|
ics_path = _get_ics_path("basic_updated.ics")
|
|
with _patch_caldav_with_events_from_ics(
|
|
ics_path,
|
|
user,
|
|
last_modified=(datetime.now(UTC)),
|
|
):
|
|
# Clear any caches to ensure fresh data
|
|
self.env["calendar.event"].invalidate_model()
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
|
|
# Refresh the event from the database to get updated values
|
|
event.invalidate_recordset()
|
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
|
|
|
# Verify the event was updated correctly
|
|
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()
|
|
notification_method = "odoo.addons.calendar.models.calendar_attendee.Attendee._send_mail_to_attendees"
|
|
# 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)
|
|
), patch(notification_method) as mock_notification_method:
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
mock_notification_method.assert_not_called()
|
|
with _patch_caldav_with_events_from_ics(
|
|
ics_path, user3, last_modified=datetime.now(UTC)
|
|
), patch(notification_method) as mock_notification_method:
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
mock_notification_method.assert_not_called()
|
|
with _patch_caldav_with_events_from_ics(
|
|
ics_path, user1, last_modified=datetime.now(UTC)
|
|
), patch(notification_method) as mock_notification_method:
|
|
self.env["calendar.event"].poll_caldav_server()
|
|
mock_notification_method.assert_not_called()
|
|
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
|