[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:
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user