[FIX] microsoft_calendar: problem creating event for another organizer

Before this commit, with the synchronization with Outlook active, it was not possible editing the organizer of an event to a different user than yourself or even creating a new event with a different organizer because Outlook didn't accept the change.

After this commit, it is possible creating a new event with this characteristic and changing the event's organizer to a different user too. Changing the organizer will delete the current event and create a new one, and creating a brand new event will create the event directly with the different organizer.

closes odoo/odoo#140820

Task-id: 3450045
X-original-commit: 81601d5
Related: odoo/enterprise#50080
Signed-off-by: Arnaud Joset (arj) <arj@odoo.com>
Signed-off-by: Gabriel de Paula Felix (gdpf) <gdpf@odoo.com>
This commit is contained in:
Gabriel de Paula Felix (gdpf)
2023-11-13 14:38:31 +00:00
parent addbc01c15
commit a2e823858b
7 changed files with 182 additions and 6 deletions
+3 -3
View File
@@ -791,12 +791,12 @@ class Meeting(models.Model):
added_partner_ids = []
for command in partner_commands:
op = command[0]
if op in (2, 3): # Remove partner
if op in (2, 3, Command.delete, Command.unlink): # Remove partner
removed_partner_ids += [command[1]]
elif op == 6: # Replace all
elif op in (6, Command.set): # Replace all
removed_partner_ids += set(self.partner_ids.ids) - set(command[2]) # Don't recreate attendee if partner already attend the event
added_partner_ids += set(command[2]) - set(self.partner_ids.ids)
elif op == 4:
elif op in (4, Command.link):
added_partner_ids += [command[1]] if command[1] not in self.partner_ids.ids else []
# commands 0 and 1 not supported
@@ -210,6 +210,15 @@ msgid ""
"%s"
msgstr ""
#. module: microsoft_calendar
#. odoo-python
#: code:addons/microsoft_calendar/models/calendar.py:0
#, python-format
msgid ""
"For having a different organizer in your event, it is necessary that the "
"organizer have its Odoo Calendar synced with Outlook Calendar."
msgstr ""
#. module: microsoft_calendar
#: model:ir.model.fields,field_description:microsoft_calendar.field_microsoft_calendar_account_reset__id
#: model:ir.model.fields,field_description:microsoft_calendar.field_microsoft_calendar_credentials__id
@@ -82,6 +82,12 @@ class Meeting(models.Model):
if self._check_microsoft_sync_status() and not notify_context and recurrency_in_batch:
self._forbid_recurrence_creation()
for vals in vals_list:
# If event has a different organizer, check its sync status and verify if the user is listed as attendee.
sender_user, partner_ids = self._get_organizer_user_change_info(vals)
partner_included = partner_ids and len(partner_ids) > 0 and sender_user.partner_id.id in partner_ids
self._check_organizer_validation(sender_user, partner_included)
# for a recurrent event, we do not create events separately but we directly
# create the recurrency from the corresponding calendar.recurrence.
# That's why, events from a recurrency have their `need_sync_m` attribute set to False.
@@ -90,6 +96,21 @@ class Meeting(models.Model):
for vals in vals_list
])
def _check_organizer_validation(self, sender_user, partner_included):
""" Check if the proposed event organizer can be set accordingly. """
# Edge case: events created or updated from Microsoft should not check organizer validation.
change_from_microsoft = self.env.context.get('dont_notify', False)
if sender_user and sender_user != self.env.user and not change_from_microsoft:
current_sync_status = self._check_microsoft_sync_status()
sender_sync_status = self.with_user(sender_user)._check_microsoft_sync_status()
if not sender_sync_status and current_sync_status:
raise ValidationError(
_("For having a different organizer in your event, it is necessary that "
"the organizer have its Odoo Calendar synced with Outlook Calendar."))
elif sender_sync_status and not partner_included:
raise ValidationError(
_("It is necessary adding the proposed organizer as attendee before saving the event."))
def _check_recurrence_overlapping(self, new_start):
"""
Outlook does not allow to modify time fields of an event if this event crosses
@@ -154,6 +175,16 @@ class Meeting(models.Model):
if not notify_context and recurrence_update_attempt and not 'active' in values:
self._forbid_recurrence_update()
# When changing the organizer, check its sync status and verify if the user is listed as attendee.
change_from_microsoft = self.env.context.get('dont_notify', False)
for event in self:
if values.get('user_id') and event.user_id.id != values['user_id'] and not change_from_microsoft:
sender_user, partner_ids = event._get_organizer_user_change_info(values)
partner_included = sender_user.partner_id in event.attendee_ids.partner_id or sender_user.partner_id.id in partner_ids
event._check_organizer_validation(sender_user, partner_included)
event._recreate_event_different_organizer(values, sender_user)
values['active'] = False
# check a Outlook limitation in overlapping the actual recurrence
if recurrence_update_setting == 'self_only' and 'start' in values:
self._check_recurrence_overlapping(values['start'])
@@ -173,6 +204,29 @@ class Meeting(models.Model):
self.recurrence_id.need_sync_m = True
return res
def _recreate_event_different_organizer(self, values, sender_user):
""" Copy current event values, delete it and recreate it with the new organizer user. """
self.ensure_one()
event_copy = {**self.copy_data()[0], 'microsoft_id': False}
self.env['calendar.event'].with_user(sender_user).create({**event_copy, **values})
if self.ms_universal_event_id:
self._microsoft_delete(self._get_organizer(), self.ms_organizer_event_id)
@api.model
def _get_organizer_user_change_info(self, values):
""" Return the sender user of the event and the partner ids listed on the event values. """
sender_user_id = values.get('user_id')
if not sender_user_id:
sender_user_id = self.env.user.id
sender_user = self.env['res.users'].browse(sender_user_id)
attendee_values = self._attendees_values(values['partner_ids']) if 'partner_ids' in values else []
partner_ids = []
if attendee_values:
for command in attendee_values:
if len(command) == 3 and isinstance(command[2], dict):
partner_ids.append(command[2].get('partner_id'))
return sender_user, partner_ids
def action_mass_archive(self, recurrence_update_setting):
# Do not allow archiving if recurrence is synced with Outlook. Suggest updating directly from Outlook.
self.ensure_one()
@@ -431,7 +431,11 @@ class MicrosoftSync(models.AbstractModel):
if not values:
return
microsoft_service = self._get_microsoft_service()
with microsoft_calendar_token(self.env.user.sudo()) as token:
# Insert using 'self.user_id' if synced with Outlook, otherwise use 'self.env.user'.
sender_user = self.env.user
if self.user_id and self.with_user(self.user_id)._check_microsoft_sync_status():
sender_user = self.user_id
with microsoft_calendar_token(sender_user.sudo()) as token:
if token:
self._ensure_attendees_have_email()
event_id, uid = microsoft_service.insert(values, token=token, timeout=timeout)
@@ -3,6 +3,8 @@ from datetime import datetime, timedelta
from markupsafe import Markup
from unittest.mock import patch, MagicMock
from odoo import fields
from odoo.tests.common import HttpCase
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftSync
@@ -56,6 +58,10 @@ class TestCommon(HttpCase):
'partner_id': partner.id,
})
# Add token validity with one hour of time window for properly checking the sync status.
for user in [self.organizer_user, self.attendee_user]:
user.microsoft_calendar_token_validity = fields.Datetime.now() + timedelta(hours=1)
# -----------------------------------------------------------------------------------------
# To create Odoo events
# -----------------------------------------------------------------------------------------
@@ -1,6 +1,8 @@
from unittest.mock import patch
from datetime import timedelta, datetime
from odoo import Command
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_calendar.models.res_users import User
@@ -379,3 +381,46 @@ class TestCreateEvents(TestCommon):
# Assert that the Microsoft Insert was called once.
mock_insert.assert_called_once()
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'insert')
def test_create_event_for_another_user(self, mock_insert, mock_get_events):
"""
Allow the creation of event for another user only if the proposed user have its Odoo Calendar synced.
User A (self.organizer_user) is creating an event with user B as organizer (self.attendee_user).
"""
# Ensure that the calendar synchronization of user A is active. Deactivate user B synchronization for throwing an error.
self.assertTrue(self.env['calendar.event'].with_user(self.organizer_user)._check_microsoft_sync_status())
self.attendee_user.microsoft_synchronization_stopped = True
# Try creating an event with the organizer as the user B (self.attendee_user).
# A ValidationError must be thrown because user B's calendar is not synced.
self.simple_event_values['user_id'] = self.attendee_user.id
self.simple_event_values['partner_ids'] = [Command.set([self.organizer_user.partner_id.id])]
with self.assertRaises(ValidationError):
self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
# Activate the calendar synchronization of user B (self.attendee_user).
self.attendee_user.microsoft_synchronization_stopped = False
self.assertTrue(self.env['calendar.event'].with_user(self.attendee_user)._check_microsoft_sync_status())
# Try creating an event with organizer as the user B but not inserting B as an attendee. A ValidationError must be thrown.
with self.assertRaises(ValidationError):
self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
# Set mock return values for the event creation.
event_id = "123"
event_iCalUId = "456"
mock_insert.return_value = (event_id, event_iCalUId)
mock_get_events.return_value = ([], None)
# Create event matching the creation conditions: user B is synced and now listed as an attendee. Set mock return values.
self.simple_event_values['partner_ids'] = [Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])]
event = self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
self.call_post_commit_hooks()
event.invalidate_recordset()
# Ensure that event was inserted and user B (self.attendee_user) is the organizer and is also listed as attendee.
mock_insert.assert_called_once()
self.assertEqual(event.user_id, self.attendee_user, "Event organizer must be user B (self.attendee_user) after event creation by user A (self.organizer_user).")
self.assertTrue(self.attendee_user.partner_id.id in event.partner_ids.ids, "User B (self.attendee_user) should be listed as attendee after event creation.")
@@ -5,12 +5,14 @@ import logging
import pytz
from unittest.mock import patch, ANY
from odoo import Command
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
from odoo.addons.microsoft_calendar.tests.common import TestCommon, mock_get_token, _modified_date_in_the_future, patch_api
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
@@ -1296,7 +1298,7 @@ class TestUpdateEvents(TestCommon):
self.assertFalse(self.env.user.microsoft_synchronization_stopped)
# Simulate upgrade of a simple event to recurrent event (forbidden).
simple_event = self.env['calendar.event'].create(self.simple_event_values)
simple_event = self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
with self.assertRaises(UserError):
simple_event.write({
'recurrency': True,
@@ -1335,3 +1337,59 @@ class TestUpdateEvents(TestCommon):
self.assertEqual(self.organizer_user._get_microsoft_sync_status(), "sync_paused")
self.assertTrue(self.simple_event.need_sync_m, "Sync variable must be true for updating event when sync re-activates")
mock_patch.assert_not_called()
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'delete')
@patch.object(MicrosoftCalendarService, 'insert')
def test_changing_event_organizer_to_another_user(self, mock_insert, mock_delete, mock_get_events):
"""
Allow editing the event organizer to another user only if the proposed organizer have its Odoo Calendar synced.
The current event is deleted and then recreated with the new organizer.
An event with organizer as user A (self.organizer_user) will have its organizer changed to user B (self.attendee_user).
"""
# Create event with organizer as user A and only the organizer as attendee.
self.assertTrue(self.env['calendar.event'].with_user(self.attendee_user)._check_microsoft_sync_status())
self.simple_event_values['user_id'] = self.organizer_user.id
self.simple_event_values['partner_ids'] = [Command.set([self.organizer_user.partner_id.id])]
event = self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
# Deactivate user B's calendar synchronization. Try changing the event organizer to user B.
# A ValidationError must be thrown because user B's calendar is not synced.
self.attendee_user.microsoft_synchronization_stopped = True
with self.assertRaises(ValidationError):
event.with_user(self.organizer_user).write({'user_id': self.attendee_user.id})
# Activate user B's calendar synchronization and try again without listing user B as an attendee.
# Another ValidationError must be thrown.
self.attendee_user.microsoft_synchronization_stopped = False
with self.assertRaises(ValidationError):
event.with_user(self.organizer_user).write({'user_id': self.attendee_user.id})
# Set mock return values for the event re-creation.
event_id = "123"
event_iCalUId = "456"
mock_insert.return_value = (event_id, event_iCalUId)
mock_get_events.return_value = ([], None)
# Change the event organizer: user B (the organizer) is synced and now listed as an attendee.
event.ms_universal_event_id = "test_id_for_event"
event.ms_organizer_event_id = "test_id_for_organizer"
event.with_user(self.organizer_user).write({
'user_id': self.attendee_user.id,
'partner_ids': [Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])]
})
new_event = self.env["calendar.event"].search([("id", ">", event.id)])
self.call_post_commit_hooks()
new_event.invalidate_recordset()
# Ensure that the event was deleted and recreated with the new organizer and the organizer listed as attendee.
mock_delete.assert_any_call(
event.ms_organizer_event_id,
token=mock_get_token(self.attendee_user),
timeout=ANY,
)
self.assertEqual(len(new_event), 1, "A single event should be created after updating the organizer.")
self.assertEqual(new_event.user_id, self.attendee_user,
"The event organizer must be user B (self.attendee_user) after the event organizer update.")
self.assertTrue(self.attendee_user.partner_id.id in new_event.partner_ids.ids,
"User B (self.attendee_user) should be listed as attendee after the event organizer update.")