bunch of new test cases and code mods
This commit is contained in:
parent
b660e4d533
commit
9243fad2e6
4 changed files with 207 additions and 24 deletions
|
|
@ -59,9 +59,9 @@ class CalendarEvent(models.Model):
|
||||||
@api.depends("user_id", "partner_ids", "partner_ids.user_id")
|
@api.depends("user_id", "partner_ids", "partner_ids.user_id")
|
||||||
def _compute_caldav_users(self):
|
def _compute_caldav_users(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.caldav_user_ids = (
|
rec.caldav_user_ids = (rec.user_id | rec.partner_ids.user_ids).filtered(
|
||||||
rec.user_id | rec.partner_ids.mapped("user_id")
|
"is_caldav_enabled"
|
||||||
).filtered(lambda user: user.is_caldav_enabled)
|
)
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
|
|
@ -249,7 +249,8 @@ class CalendarEvent(models.Model):
|
||||||
"""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 = [item[1] for item in component.property_items() if item[0] == "RRULE"]
|
||||||
|
rrule = rrule[0] if rrule else None
|
||||||
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
|
||||||
|
|
@ -268,7 +269,7 @@ class CalendarEvent(models.Model):
|
||||||
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 != 1:
|
||||||
# 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 {}
|
||||||
|
|
||||||
|
|
@ -315,15 +316,19 @@ class CalendarEvent(models.Model):
|
||||||
partner_ids = self._get_attendee_partners(component, current_user_email)
|
partner_ids = self._get_attendee_partners(component, current_user_email)
|
||||||
|
|
||||||
existing_instance = self._get_existing_instance(uid, recurrence_id)
|
existing_instance = self._get_existing_instance(uid, recurrence_id)
|
||||||
|
outdated = False
|
||||||
last_modified = component.decoded("last-modified")
|
last_modified = component.decoded("last-modified")
|
||||||
if existing_instance and last_modified:
|
if existing_instance and last_modified:
|
||||||
last_modified = last_modified.astimezone(utc).replace(tzinfo=None)
|
last_modified = last_modified.astimezone(utc).replace(tzinfo=None)
|
||||||
if last_modified < existing_instance.write_date:
|
if last_modified < existing_instance.write_date:
|
||||||
_logger.info(
|
# _logger.info(
|
||||||
f"Last modified date {last_modified} is before most recent "
|
# f"Last modified date {last_modified} is before most recent "
|
||||||
f"write date {existing_instance.write_date}. Skipping."
|
# f"write date {existing_instance.write_date}. Skipping."
|
||||||
)
|
# )
|
||||||
continue
|
outdated = True
|
||||||
|
owned = (
|
||||||
|
existing_instance and existing_instance.partner_id == user.partner_id
|
||||||
|
)
|
||||||
values, recurrency_vals = (
|
values, recurrency_vals = (
|
||||||
self._get_vals_recurrency_vals_from_ical_component(
|
self._get_vals_recurrency_vals_from_ical_component(
|
||||||
partner_ids, component, user
|
partner_ids, component, user
|
||||||
|
|
@ -333,6 +338,14 @@ class CalendarEvent(models.Model):
|
||||||
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)
|
||||||
|
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" Skipping."
|
||||||
|
)
|
||||||
|
pass # Do nothing, it's not this user's event to modify or it's outdated
|
||||||
else:
|
else:
|
||||||
# Don't update partner_ids if no change
|
# Don't update partner_ids if no change
|
||||||
if partner_ids != existing_instance.partner_ids:
|
if partner_ids != existing_instance.partner_ids:
|
||||||
|
|
@ -473,13 +486,3 @@ class CalendarEvent(models.Model):
|
||||||
"tentative": "TENTATIVE",
|
"tentative": "TENTATIVE",
|
||||||
}
|
}
|
||||||
return mapping.get(state, "NEEDS-ACTION")
|
return mapping.get(state, "NEEDS-ACTION")
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _map_ical_status(ical_status):
|
|
||||||
mapping = {
|
|
||||||
"NEEDS-ACTION": "needsAction",
|
|
||||||
"ACCEPTED": "accepted",
|
|
||||||
"DECLINED": "declined",
|
|
||||||
"TENTATIVE": "tentative",
|
|
||||||
}
|
|
||||||
return mapping.get(ical_status, "needsAction")
|
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,15 @@ class CaldavTestCommon:
|
||||||
def _generate_user(
|
def _generate_user(
|
||||||
cls, name, caldav_username=None, caldav_password=None, caldav_url=None
|
cls, name, caldav_username=None, caldav_password=None, caldav_url=None
|
||||||
):
|
):
|
||||||
|
groups_ids = cls.env.ref("base.group_user") | cls.env.ref(
|
||||||
|
"base.group_partner_manager"
|
||||||
|
)
|
||||||
vals = {
|
vals = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"login": name,
|
"login": name,
|
||||||
"password": name,
|
"password": name,
|
||||||
"email": name + "@example.com",
|
"email": name + "@example.com",
|
||||||
"groups_id": [Command.set(cls.env.ref("base.group_user").ids)],
|
"groups_id": [Command.set(groups_ids.ids)],
|
||||||
}
|
}
|
||||||
if caldav_username:
|
if caldav_username:
|
||||||
vals.update(caldav_username=caldav_username)
|
vals.update(caldav_username=caldav_username)
|
||||||
|
|
|
||||||
38
caldav_sync/tests/data/test_recurring.ics
Normal file
38
caldav_sync/tests/data/test_recurring.ics
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
VERSION:2.0
|
||||||
|
X-WR-CALNAME:test recurring
|
||||||
|
METHOD:PUBLISH
|
||||||
|
PRODID:-//Apple Inc.//macOS 14.5//EN
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:America/Toronto
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZOFFSETFROM:-0500
|
||||||
|
DTSTART:20070311T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||||
|
TZNAME:EDT
|
||||||
|
TZOFFSETTO:-0400
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZOFFSETFROM:-0400
|
||||||
|
DTSTART:20071104T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||||
|
TZNAME:EST
|
||||||
|
TZOFFSETTO:-0500
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
DTEND;TZID=America/Toronto:20241008T040000
|
||||||
|
X-APPLE-CREATOR-TEAM-IDENTITY:0000000000
|
||||||
|
UID:F777C16F-2F14-4326-9681-69C6043DB8C0
|
||||||
|
DTSTAMP:20241007T195255Z
|
||||||
|
X-APPLE-CREATOR-IDENTITY:com.apple.calendar
|
||||||
|
SEQUENCE:1
|
||||||
|
SUMMARY:test recurring
|
||||||
|
DTSTART;TZID=America/Toronto:20241008T030000
|
||||||
|
LAST-MODIFIED:20241007T195252Z
|
||||||
|
CREATED:20241007T195223Z
|
||||||
|
RRULE:FREQ=WEEKLY;COUNT=10
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
|
@ -8,6 +8,16 @@ from .common import CaldavTestCommon
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import datetime, UTC, timedelta
|
from datetime import datetime, UTC, timedelta
|
||||||
|
|
||||||
|
WEEKDAY_MAP = {
|
||||||
|
0: "SUN",
|
||||||
|
1: "MON",
|
||||||
|
2: "TUE",
|
||||||
|
3: "WED",
|
||||||
|
4: "THU",
|
||||||
|
5: "FRI",
|
||||||
|
6: "SAT",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_ics_path(filename):
|
def _get_ics_path(filename):
|
||||||
return Path(__file__).parent / "data" / filename
|
return Path(__file__).parent / "data" / filename
|
||||||
|
|
@ -23,10 +33,13 @@ def _patch_caldav_with_events_from_ics(ics_paths, user, last_modified=None):
|
||||||
mock_client = MockDAVClient.return_value
|
mock_client = MockDAVClient.return_value
|
||||||
mock_calendar = MockCalendar.return_value
|
mock_calendar = MockCalendar.return_value
|
||||||
mock_client.calendar = mock_calendar
|
mock_client.calendar = mock_calendar
|
||||||
|
mock_calendars = {}
|
||||||
|
|
||||||
def calendar_side_effect(url):
|
def calendar_side_effect(url):
|
||||||
|
if url not in mock_calendars:
|
||||||
|
mock_calendars[url] = MockCalendar()
|
||||||
if url == user.caldav_calendar_url:
|
if url == user.caldav_calendar_url:
|
||||||
return mock_calendar
|
return mock_calendars[url]
|
||||||
raise Exception("Calendar does not exist.")
|
raise Exception("Calendar does not exist.")
|
||||||
|
|
||||||
mock_calendar.side_effect = calendar_side_effect
|
mock_calendar.side_effect = calendar_side_effect
|
||||||
|
|
@ -109,6 +122,16 @@ class TestCalendarEvent(TransactionCase, CaldavTestCommon):
|
||||||
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
event = self.env["calendar.event"].search([("user_id", "=", user.id)])
|
||||||
self.assertFalse(event)
|
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):
|
def test_multiple_attendees_event_from_server_create(self):
|
||||||
user = self.user_1
|
user = self.user_1
|
||||||
ics_path = _get_ics_path("test_multi_attendee.ics")
|
ics_path = _get_ics_path("test_multi_attendee.ics")
|
||||||
|
|
@ -154,9 +177,125 @@ class TestCalendarEvent(TransactionCase, CaldavTestCommon):
|
||||||
ics_path = _get_ics_path("test_multi_user.ics")
|
ics_path = _get_ics_path("test_multi_user.ics")
|
||||||
with _patch_caldav_with_events_from_ics(ics_path, user1):
|
with _patch_caldav_with_events_from_ics(ics_path, user1):
|
||||||
self.env["calendar.event"].poll_caldav_server()
|
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):
|
def test_multiple_user_attendees_event_from_server_update(self):
|
||||||
pass
|
"""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),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_multiple_user_attendees_event_to_server_create(self):
|
def test_multiple_user_attendees_event_to_server_create(self):
|
||||||
pass
|
with self._patch_all_3_users_davclients() as (_, mock_calendar):
|
||||||
|
self._create_multi_user_test_event()
|
||||||
|
self.assertEqual(mock_calendar.add_event.call_count, 3)
|
||||||
|
|
||||||
|
def test_event_to_server_delete(self):
|
||||||
|
with self._patch_all_3_users_davclients() as (_, mock_calendar):
|
||||||
|
self._create_multi_user_test_event().unlink()
|
||||||
|
self.assertEqual(
|
||||||
|
mock_calendar.event_by_uid.return_value.delete.call_count, 3
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_event_to_server_update(self):
|
||||||
|
with self._patch_all_3_users_davclients() as (_, mock_calendar):
|
||||||
|
self._create_multi_user_test_event().write(
|
||||||
|
{"start": datetime.now() + timedelta(days=14)}
|
||||||
|
)
|
||||||
|
self.assertEqual(mock_calendar.save_event.call_count, 3)
|
||||||
|
|
||||||
|
def test_recurrent_event_to_server(self):
|
||||||
|
with self._patch_all_3_users_davclients() as (_, mock_calendar):
|
||||||
|
self._create_multi_user_test_event().write(
|
||||||
|
{
|
||||||
|
"recurrency": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
args = mock_calendar.save_event.call_args
|
||||||
|
self.assertEqual(mock_calendar.save_event.call_count, 3)
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue