Files
odoo_source/addons/microsoft_calendar/tests/common.py
T
Rémy Baranx (bar) 77e89e893c [FIX] microsoft_calendar: issue when archiving several events
In the `write` method, accessing the `ms_organizer_event_id` field of
`self` leads to an exception if `self` represents several records.

So, the idea of this fix is to set the `need_sync_m` field of all modified
records to `True` when at least a field to sync with Outlook is modified
(see `_get_microsoft_synced_fields()`) and then, at the end of the `write`
method, really patch or delete records that are already linked to Outlook
(that means they already have their `ms_organizer_event_id` field set).

closes odoo/odoo#96761

X-original-commit: 1a66343327f9ea9f59ba25ea63e26331cabb65b5
Signed-off-by: Arnaud Joset <arj@odoo.com>
2022-07-27 19:20:15 +02:00

516 lines
22 KiB
Python

import pytz
from datetime import datetime, timedelta
from markupsafe import Markup
from unittest.mock import patch, MagicMock
from odoo.tests.common import HttpCase
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftSync
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
def mock_get_token(user):
return f"TOKEN_FOR_USER_{user.id}"
def _modified_date_in_the_future(event):
"""
Add some seconds to the event write date to be sure to have a last modified date
in the future
"""
return (event.write_date + timedelta(seconds=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
def patch_api(func):
@patch.object(MicrosoftSync, '_microsoft_insert', MagicMock())
@patch.object(MicrosoftSync, '_microsoft_delete', MagicMock())
@patch.object(MicrosoftSync, '_microsoft_patch', MagicMock())
def patched(self, *args, **kwargs):
return func(self, *args, **kwargs)
return patched
# By inheriting from TransactionCase, postcommit hooks (so methods tagged with `@after_commit` in MicrosoftSync),
# are not called because no commit is done.
# To be able to manually call these postcommit hooks, we need to inherit from HttpCase.
# Note: as postcommit hooks are called separately, do not forget to invalidate cache for records read during the test.
class TestCommon(HttpCase):
@patch_api
def setUp(self):
super(TestCommon, self).setUp()
# prepare users
self.organizer_user = self.env["res.users"].search([("name", "=", "Mike Organizer")])
if not self.organizer_user:
partner = self.env['res.partner'].create({'name': 'Mike Organizer', 'email': 'mike@organizer.com'})
self.organizer_user = self.env['res.users'].create({
'name': 'Mike Organizer',
'login': 'mike@organizer.com',
'partner_id': partner.id,
})
self.attendee_user = self.env["res.users"].search([("name", "=", "John Attendee")])
if not self.attendee_user:
partner = self.env['res.partner'].create({'name': 'John Attendee', 'email': 'john@attendee.com'})
self.attendee_user = self.env['res.users'].create({
'name': 'John Attendee',
'login': 'john@attendee.com',
'partner_id': partner.id,
})
# -----------------------------------------------------------------------------------------
# To create Odoo events
# -----------------------------------------------------------------------------------------
self.start_date = datetime(2021, 9, 22, 10, 0, 0, 0)
self.end_date = datetime(2021, 9, 22, 11, 0, 0, 0)
self.recurrent_event_interval = 2
self.recurrent_events_count = 7
self.recurrence_end_date = self.end_date + timedelta(
days=self.recurrent_event_interval * self.recurrent_events_count
)
# simple event values to create a Odoo event
self.simple_event_values = {
"name": "simple_event",
"description": "my simple event",
"active": True,
"start": self.start_date,
"stop": self.end_date,
"partner_ids": [(4, self.organizer_user.partner_id.id), (4, self.attendee_user.partner_id.id)],
}
self.recurrent_event_values = {
'name': 'recurring_event',
'description': 'a recurring event',
"partner_ids": [(4, self.attendee_user.partner_id.id)],
'recurrency': True,
'follow_recurrence': True,
'start': self.start_date.strftime("%Y-%m-%d %H:%M:%S"),
'stop': self.end_date.strftime("%Y-%m-%d %H:%M:%S"),
'event_tz': 'Europe/London',
'recurrence_update': 'self_only',
'rrule_type': 'daily',
'interval': self.recurrent_event_interval,
'count': self.recurrent_events_count,
'end_type': 'count',
'duration': 1,
'byday': '-1',
'day': 22,
'wed': True,
'weekday': 'WED'
}
# -----------------------------------------------------------------------------------------
# Expected values for Odoo events converted to Outlook events (to be posted through API)
# -----------------------------------------------------------------------------------------
# simple event values converted in the Outlook format to be posted through the API
self.simple_event_ms_values = {
"subject": self.simple_event_values["name"],
"body": {
'content': self.simple_event_values["description"],
'contentType': "text",
},
"start": {
'dateTime': pytz.utc.localize(self.simple_event_values["start"]).isoformat(),
'timeZone': 'Europe/London'
},
"end": {
'dateTime': pytz.utc.localize(self.simple_event_values["stop"]).isoformat(),
'timeZone': 'Europe/London'
},
"isAllDay": False,
"organizer": {
'emailAddress': {
'address': self.organizer_user.email,
'name': self.organizer_user.display_name,
}
},
"isOrganizer": True,
"sensitivity": "normal",
"showAs": "busy",
"attendees": [
{
'emailAddress': {
'address': self.attendee_user.email,
'name': self.attendee_user.display_name
},
'status': {'response': "notresponded"}
}
],
"isReminderOn": False,
"location": {'displayName': ''},
"reminderMinutesBeforeStart": 0,
}
self.recurrent_event_ms_values = {
'subject': self.recurrent_event_values["name"],
"body": {
'content': Markup('<p>%s</p>' % self.recurrent_event_values["description"]),
'contentType': "html",
},
'start': {
'dateTime': self.start_date.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
'timeZone': 'Europe/London'
},
'end': {
'dateTime': self.end_date.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
'timeZone': 'Europe/London'
},
'isAllDay': False,
'isOrganizer': True,
'isReminderOn': False,
'reminderMinutesBeforeStart': 0,
'sensitivity': 'normal',
'showAs': 'busy',
'type': 'seriesMaster',
"attendees": [
{
'emailAddress': {
'address': self.attendee_user.email,
'name': self.attendee_user.display_name
},
'status': {'response': "notresponded"}
}
],
'location': {'displayName': ''},
'organizer': {
'emailAddress': {
'address': self.organizer_user.email,
'name': self.organizer_user.display_name,
},
},
'recurrence': {
'pattern': {'dayOfMonth': 22, 'interval': self.recurrent_event_interval, 'type': 'daily'},
'range': {
'numberOfOccurrences': self.recurrent_events_count,
'startDate': self.start_date.strftime("%Y-%m-%d"),
'type': 'numbered'
},
},
}
# -----------------------------------------------------------------------------------------
# Events coming from Outlook (so from the API)
# -----------------------------------------------------------------------------------------
self.simple_event_from_outlook_organizer = {
'type': 'singleInstance',
'seriesMasterId': None,
'id': '123',
'iCalUId': '456',
'subject': 'simple_event',
'body': {
'content': "my simple event",
'contentType': "text",
},
'start': {'dateTime': self.start_date.strftime("%Y-%m-%dT%H:%M:%S.0000000"), 'timeZone': 'UTC'},
'end': {'dateTime': self.end_date.strftime("%Y-%m-%dT%H:%M:%S.0000000"), 'timeZone': 'UTC'},
'attendees': [{
'type': 'required',
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
'emailAddress': {'name': self.attendee_user.display_name, 'address': self.attendee_user.email}
}],
'isAllDay': False,
'isCancelled': False,
'sensitivity': 'normal',
'showAs': 'busy',
'isOnlineMeeting': False,
'onlineMeetingUrl': None,
'isOrganizer': True,
'isReminderOn': True,
'location': {'displayName': ''},
'organizer': {
'emailAddress': {'address': self.organizer_user.email, 'name': self.organizer_user.display_name},
},
'reminderMinutesBeforeStart': 15,
'responseRequested': True,
'responseStatus': {
'response': 'organizer',
'time': '0001-01-01T00:00:00Z',
},
}
self.simple_event_from_outlook_attendee = self.simple_event_from_outlook_organizer
self.simple_event_from_outlook_attendee.update(isOrganizer=False)
# -----------------------------------------------------------------------------------------
# Expected values for Outlook events converted to Odoo events
# -----------------------------------------------------------------------------------------
self.expected_odoo_event_from_outlook = {
"name": "simple_event",
"description": Markup('<p>my simple event</p>'),
"active": True,
"start": self.start_date,
"stop": self.end_date,
"user_id": self.organizer_user,
"microsoft_id": combine_ids("123", "456"),
"partner_ids": [self.organizer_user.partner_id.id, self.attendee_user.partner_id.id],
}
self.expected_odoo_recurrency_from_outlook = {
'active': True,
'byday': '1',
'count': 0,
'day': 0,
'display_name': "Every %s Days until %s" % (
self.recurrent_event_interval, self.recurrence_end_date.strftime("%Y-%m-%d")
),
'dtstart': self.start_date,
'end_type': 'end_date',
'event_tz': False,
'fri': False,
'interval': self.recurrent_event_interval,
'month_by': 'date',
'microsoft_id': combine_ids('REC123', 'REC456'),
'name': "Every %s Days until %s" % (
self.recurrent_event_interval, self.recurrence_end_date.strftime("%Y-%m-%d")
),
'need_sync_m': False,
'rrule': 'DTSTART:%s\nRRULE:FREQ=DAILY;INTERVAL=%s;UNTIL=%s' % (
self.start_date.strftime("%Y%m%dT%H%M%S"),
self.recurrent_event_interval,
self.recurrence_end_date.strftime("%Y%m%dT235959"),
),
'rrule_type': 'daily',
'until': self.recurrence_end_date.date(),
'weekday': False,
}
self.recurrent_event_from_outlook_organizer = [{
'attendees': [{
'emailAddress': {'address': self.attendee_user.email, 'name': self.attendee_user.display_name},
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
'type': 'required'
}],
'body': {
'content': "my recurrent event",
'contentType': "text",
},
'start': {'dateTime': self.start_date.strftime("%Y-%m-%dT%H:%M:%S.0000000"), 'timeZone': 'UTC'},
'end': {'dateTime': self.end_date.strftime("%Y-%m-%dT%H:%M:%S.0000000"), 'timeZone': 'UTC'},
'id': 'REC123',
'iCalUId': 'REC456',
'isAllDay': False,
'isCancelled': False,
'isOnlineMeeting': False,
'isOrganizer': True,
'isReminderOn': True,
'location': {'displayName': ''},
'organizer': {'emailAddress': {
'address': self.organizer_user.email, 'name': self.organizer_user.display_name}
},
'recurrence': {
'pattern': {
'dayOfMonth': 0,
'firstDayOfWeek': 'sunday',
'index': 'first',
'interval': self.recurrent_event_interval,
'month': 0,
'type': 'daily'
},
'range': {
'startDate': self.start_date.strftime("%Y-%m-%d"),
'endDate': self.recurrence_end_date.strftime("%Y-%m-%d"),
'numberOfOccurrences': 0,
'recurrenceTimeZone': 'Romance Standard Time',
'type': 'endDate'
}
},
'reminderMinutesBeforeStart': 15,
'responseRequested': True,
'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'},
'sensitivity': 'normal',
'seriesMasterId': None,
'showAs': 'busy',
'subject': "recurrent event",
'type': 'seriesMaster',
}]
self.recurrent_event_from_outlook_organizer += [
{
'attendees': [{
'emailAddress': {'address': self.attendee_user.email, 'name': self.attendee_user.display_name},
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
'type': 'required'
}],
'body': {
'content': "my recurrent event",
'contentType': "text",
},
'start': {
'dateTime': (
self.start_date + timedelta(days=i * self.recurrent_event_interval)
).strftime("%Y-%m-%dT%H:%M:%S.0000000"),
'timeZone': 'UTC'
},
'end': {
'dateTime': (
self.end_date + timedelta(days=i * self.recurrent_event_interval)
).strftime("%Y-%m-%dT%H:%M:%S.0000000"),
'timeZone': 'UTC'
},
'id': f'REC123_EVENT_{i+1}',
'iCalUId': f'REC456_EVENT_{i+1}',
'seriesMasterId': 'REC123',
'isAllDay': False,
'isCancelled': False,
'isOnlineMeeting': False,
'isOrganizer': True,
'isReminderOn': True,
'location': {'displayName': ''},
'organizer': {
'emailAddress': {'address': self.organizer_user.email, 'name': self.organizer_user.display_name}
},
'recurrence': None,
'reminderMinutesBeforeStart': 15,
'responseRequested': True,
'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'},
'sensitivity': 'normal',
'showAs': 'busy',
'subject': "recurrent event",
'type': 'occurrence',
}
for i in range(self.recurrent_events_count)
]
self.recurrent_event_from_outlook_attendee = [
dict(
d,
isOrganizer=False,
attendees=[
{
'emailAddress': {'address': self.organizer_user.email, 'name': self.organizer_user.display_name},
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
'type': 'required'
},
{
'emailAddress': {'address': self.attendee_user.email, 'name': self.attendee_user.display_name},
'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'},
'type': 'required'
},
]
)
for d in self.recurrent_event_from_outlook_organizer
]
self.expected_odoo_recurrency_events_from_outlook = [
{
"name": "recurrent event",
"user_id": self.organizer_user,
"partner_ids": [self.organizer_user.partner_id.id, self.attendee_user.partner_id.id],
"start": self.start_date + timedelta(days=i * self.recurrent_event_interval),
"stop": self.end_date + timedelta(days=i * self.recurrent_event_interval),
"until": self.recurrence_end_date.date(),
"microsoft_recurrence_master_id": "REC123",
'microsoft_id': combine_ids(f"REC123_EVENT_{i+1}", f"REC456_EVENT_{i+1}"),
"recurrency": True,
"follow_recurrence": True,
"active": True,
}
for i in range(self.recurrent_events_count)
]
self.env.cr.postcommit.clear()
def create_events_for_tests(self):
"""
Create some events for test purpose
"""
# ---- create some events that will be updated during tests -----
# a simple event
self.simple_event = self.env["calendar.event"].search([("name", "=", "simple_event")])
if not self.simple_event:
self.simple_event = self.env["calendar.event"].with_user(self.organizer_user).create(
dict(
self.simple_event_values,
microsoft_id=combine_ids("123", "456"),
)
)
# a group of events
self.several_events = self.env["calendar.event"].search([("name", "like", "event%")])
if not self.several_events:
self.several_events = self.env["calendar.event"].with_user(self.organizer_user).create([
dict(
self.simple_event_values,
name=f"event{i}",
microsoft_id=combine_ids(f"e{i}", f"u{i}"),
)
for i in range(1, 4)
])
# a recurrent event with 7 occurrences
self.recurrent_base_event = self.env["calendar.event"].search(
[("name", "=", "recurrent_event")],
order="id",
limit=1,
)
already_created = self.recurrent_base_event
if not already_created:
self.recurrent_base_event = self.env["calendar.event"].with_user(self.organizer_user).create(
self.recurrent_event_values
)
self.recurrence = self.env["calendar.recurrence"].search([("base_event_id", "=", self.recurrent_base_event.id)])
# set ids set by Outlook
if not already_created:
self.recurrence.write({
"microsoft_id": combine_ids("REC123", "REC456"),
})
for i, e in enumerate(self.recurrence.calendar_event_ids.sorted(key=lambda r: r.start)):
e.write({
"microsoft_id": combine_ids(f"REC123_EVENT_{i+1}", f"REC456_EVENT_{i+1}"),
"microsoft_recurrence_master_id": "REC123",
})
self.recurrence.invalidate_recordset()
self.recurrence.calendar_event_ids.invalidate_recordset()
self.recurrent_events = self.recurrence.calendar_event_ids.sorted(key=lambda r: r.start)
self.recurrent_events_count = len(self.recurrent_events)
def assert_odoo_event(self, odoo_event, expected_values):
"""
Assert that an Odoo event has the same values than in the expected_values dictionary,
for the keys present in expected_values.
"""
self.assertTrue(expected_values)
odoo_event_values = odoo_event.read(list(expected_values.keys()))[0]
for k, v in expected_values.items():
if k in ("user_id", "recurrence_id"):
v = (v.id, v.name) if v else False
if isinstance(v, list):
self.assertListEqual(sorted(v), sorted(odoo_event_values.get(k)), msg=f"'{k}' mismatch")
else:
self.assertEqual(v, odoo_event_values.get(k), msg=f"'{k}' mismatch")
def assert_odoo_recurrence(self, odoo_recurrence, expected_values):
"""
Assert that an Odoo recurrence has the same values than in the expected_values dictionary,
for the keys present in expected_values.
"""
odoo_recurrence_values = odoo_recurrence.read(list(expected_values.keys()))[0]
for k, v in expected_values.items():
self.assertEqual(v, odoo_recurrence_values.get(k), msg=f"'{k}' mismatch")
def assert_dict_equal(self, dict1, dict2):
# check missing keys
keys = set(dict1.keys()) ^ set(dict2.keys())
self.assertFalse(keys, msg="Following keys are not in both dicts: %s" % ", ".join(keys))
# compare key by key
for k, v in dict1.items():
self.assertEqual(v, dict2.get(k), f"'{k}' mismatch")
def call_post_commit_hooks(self):
"""
manually calls postcommit hooks defined with the decorator @after_commit
"""
# need to manually handle post-commit hooks calls as `self.env.cr.postcommit.run()` clean
# the queue at the end of the first post-commit hook call ...
funcs = self.env.cr.postcommit._funcs.copy()
while funcs:
func = funcs.popleft()
func()