[IMP] microsoft_calendar: Credentials module and don't sync old events

Before this commit, when the first synchronization with Outlook Calendar was done or when the synchronization was restarted, all events previously created in Odoo (before synchronizing) were being synced with Outlook, triggering lots of emails and notifications for attendees. Additionaly, all microsoft credentials information were concentrated within the res_users file, difficulting updates in this file.

After this commit, only events created or updated after the synchronization starts (or restarts) are synced with Outlook Calendar, with 5 minutes of time acceptance for avoiding network delay issues. Moreover, a Microsoft Credentials module was added for gathering microsoft credentials in one place.

closes odoo/odoo#128150

Task-id: 3410651
Related: odoo/upgrade#5047
Signed-off-by: Arnaud Joset (arj) <arj@odoo.com>
This commit is contained in:
Gabriel de Paula Felix (gdpf)
2023-09-12 20:52:14 +00:00
parent 3f55f57c44
commit 69d23262f8
11 changed files with 186 additions and 14 deletions
@@ -1,5 +1,9 @@
-- neutralization of Microsoft calendar
UPDATE res_users
SET microsoft_calendar_token = NULL,
microsoft_calendar_rtoken = NULL,
microsoft_synchronization_stopped = True;
microsoft_calendar_rtoken = NULL;
UPDATE microsoft_calendar_credentials
SET calendar_sync_token = NULL,
synchronization_stopped = True,
last_sync_date = NULL;
@@ -7,3 +7,4 @@ from . import calendar
from . import calendar_recurrence_rule
from . import res_users
from . import calendar_attendee
from . import microsoft_credentials
+10 -2
View File
@@ -4,7 +4,7 @@
import logging
import pytz
import re
from datetime import datetime
from datetime import datetime, timedelta
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
@@ -12,6 +12,7 @@ from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import is_html_empty, email_normalize
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
from odoo.osv import expression
ATTENDEE_CONVERTER_O2M = {
'needsAction': 'notresponded',
@@ -53,7 +54,14 @@ class Meeting(models.Model):
@api.model
def _restart_microsoft_sync(self):
self.env['calendar.event'].search(self._get_microsoft_sync_domain()).write({
domain = self._get_microsoft_sync_domain()
# Sync only events created/updated after last sync date (with 5 min of time acceptance).
if self.env.user.microsoft_last_sync_date:
time_offset = timedelta(minutes=5)
domain = expression.AND([domain, [('write_date', '>=', self.env.user.microsoft_last_sync_date - time_offset)]])
self.env['calendar.event'].search(domain).write({
'need_sync_m': True,
})
@@ -0,0 +1,16 @@
import logging
from odoo import fields, models
_logger = logging.getLogger(__name__)
class MicrosoftCredentials(models.Model):
""""Microsoft Account of res_users"""
_name = 'microsoft.calendar.credentials'
_description = 'Microsoft Calendar Account Data'
user_ids = fields.One2many('res.users', 'microsoft_calendar_account_id', required=True)
calendar_sync_token = fields.Char('Microsoft Next Sync Token', copy=False)
synchronization_stopped = fields.Boolean('Outlook Synchronization stopped', copy=False)
last_sync_date = fields.Datetime('Last Sync Date', copy=False, help='Last synchronization date with Outlook Calendar')
@@ -6,6 +6,7 @@ from contextlib import contextmanager
from functools import wraps
import pytz
from dateutil.parser import parse
from datetime import timedelta
from odoo import api, fields, models, registry
from odoo.exceptions import UserError
@@ -519,4 +520,8 @@ class MicrosoftSync(models.AbstractModel):
'&', ('ms_universal_event_id', '=', False), is_active_clause,
('need_sync_m', '=', True),
]])
# Sync only events created/updated after last sync date (with 5 min of time acceptance).
if self.env.user.microsoft_last_sync_date:
time_offset = timedelta(minutes=5)
domain = expression.AND([domain, [('write_date', '>=', self.env.user.microsoft_last_sync_date - time_offset)]])
return domain
+31 -7
View File
@@ -6,7 +6,7 @@ import requests
from odoo.addons.microsoft_calendar.models.microsoft_sync import microsoft_calendar_token
from datetime import timedelta
from odoo import api, fields, models, _
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError
from odoo.loglevels import exception_to_unicode
from odoo.addons.microsoft_account.models.microsoft_service import DEFAULT_MICROSOFT_TOKEN_ENDPOINT
@@ -19,16 +19,22 @@ _logger = logging.getLogger(__name__)
class User(models.Model):
_inherit = 'res.users'
microsoft_calendar_sync_token = fields.Char('Microsoft Next Sync Token', copy=False)
microsoft_synchronization_stopped = fields.Boolean('Outlook Synchronization stopped', copy=False)
microsoft_calendar_account_id = fields.Many2one('microsoft.calendar.credentials')
microsoft_calendar_sync_token = fields.Char(related='microsoft_calendar_account_id.calendar_sync_token')
microsoft_synchronization_stopped = fields.Boolean(related='microsoft_calendar_account_id.synchronization_stopped', readonly=False)
microsoft_last_sync_date = fields.Datetime(related='microsoft_calendar_account_id.last_sync_date', readonly=False)
_sql_constraints = [
('microsoft_token_uniq', 'unique (microsoft_calendar_account_id)', "The user has already a microsoft account"),
]
@property
def SELF_READABLE_FIELDS(self):
return super().SELF_READABLE_FIELDS + ['microsoft_synchronization_stopped']
return super().SELF_READABLE_FIELDS + ['microsoft_synchronization_stopped', 'microsoft_calendar_account_id', 'microsoft_last_sync_date']
@property
def SELF_WRITEABLE_FIELDS(self):
return super().SELF_WRITEABLE_FIELDS + ['microsoft_synchronization_stopped']
return super().SELF_WRITEABLE_FIELDS + ['microsoft_synchronization_stopped', 'microsoft_calendar_account_id', 'microsoft_last_sync_date']
def _microsoft_calendar_authenticated(self):
return bool(self.sudo().microsoft_calendar_rtoken)
@@ -79,7 +85,9 @@ class User(models.Model):
'microsoft_calendar_rtoken': False,
'microsoft_calendar_token': False,
'microsoft_calendar_token_validity': False,
'microsoft_calendar_sync_token': False,
})
self.microsoft_calendar_account_id.write({
'calendar_sync_token': False
})
self.env.cr.commit()
error_key = error.response.json().get("error", "nc")
@@ -100,6 +108,10 @@ class User(models.Model):
def _sync_microsoft_calendar(self):
self.ensure_one()
# Create the Calendar Credentials if it is not defined yet and define the last sync date after creation.
if not self.microsoft_calendar_account_id and self._check_microsoft_calendar_credentials():
self.microsoft_last_sync_date = fields.datetime.now()
if self._get_microsoft_sync_status() != "sync_active":
return False
calendar_service = self.env["calendar.event"]._get_microsoft_service()
@@ -110,7 +122,7 @@ class User(models.Model):
except InvalidSyncToken:
events, next_sync_token = calendar_service.get_events(token=token)
full_sync = True
self.microsoft_calendar_sync_token = next_sync_token
self.microsoft_calendar_account_id.calendar_sync_token = next_sync_token
# Microsoft -> Odoo
synced_events, synced_recurrences = self.env['calendar.event']._sync_microsoft2odoo(events) if events else (self.env['calendar.event'], self.env['calendar.recurrence'])
@@ -123,6 +135,7 @@ class User(models.Model):
events = self.env['calendar.event']._get_microsoft_records_to_sync(full_sync=full_sync)
(events - synced_events)._sync_odoo2microsoft()
self.microsoft_last_sync_date = fields.datetime.now()
return bool(events | synced_events) or bool(recurrences | synced_recurrences)
@@ -139,12 +152,23 @@ class User(models.Model):
_logger.exception("[%s] Calendar Synchro - Exception : %s!", user, exception_to_unicode(e))
self.env.cr.rollback()
def _check_microsoft_calendar_credentials(self):
# Create the Calendar Credentials for the current user if not already created.
self.ensure_one()
if not self.microsoft_calendar_account_id:
self.microsoft_calendar_account_id = self.env['microsoft.calendar.credentials'].sudo().create([{'user_ids': [Command.set(self.ids)]}])
return True
def stop_microsoft_synchronization(self):
self.ensure_one()
self._check_microsoft_calendar_credentials()
self.microsoft_synchronization_stopped = True
self.microsoft_last_sync_date = None
def restart_microsoft_synchronization(self):
self.ensure_one()
self._check_microsoft_calendar_credentials()
self.microsoft_last_sync_date = fields.datetime.now()
self.microsoft_synchronization_stopped = False
self.env['calendar.recurrence']._restart_microsoft_sync()
self.env['calendar.event']._restart_microsoft_sync()
@@ -1,2 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
microsoft_calendar_account_reset,microsoft_calendar_account_reset_access_right,model_microsoft_calendar_account_reset,base.group_system,1,1,1,0
microsoft_calendar.access_microsoft_calendar_credentials,access_microsoft_calendar_credentials,microsoft_calendar.model_microsoft_calendar_credentials,base.group_user,1,1,0,0
microsoft_calendar_manager,access_microsoft_calendar_credentials_manager,microsoft_calendar.model_microsoft_calendar_credentials,base.group_system,1,1,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 microsoft_calendar_account_reset microsoft_calendar_account_reset_access_right model_microsoft_calendar_account_reset base.group_system 1 1 1 0
3 microsoft_calendar.access_microsoft_calendar_credentials access_microsoft_calendar_credentials microsoft_calendar.model_microsoft_calendar_credentials base.group_user 1 1 0 0
4 microsoft_calendar_manager access_microsoft_calendar_credentials_manager microsoft_calendar.model_microsoft_calendar_credentials base.group_system 1 1 0 0
@@ -0,0 +1,30 @@
<odoo>
<data noupdate="1">
<record id="microsoft_calendar_not_own_token_rule" model="ir.rule">
<field name="name">Microsoft Calendar NOT own token access rule</field>
<field name="model_id" ref="model_microsoft_calendar_credentials"/>
<field name="domain_force">[('user_ids', 'not in', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_create" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_read" eval="1"/>
</record>
<record id="microsoft_calendar_own_token_rule" model="ir.rule">
<field name="name">Microsoft Calendar own token access rule</field>
<field name="model_id" ref="model_microsoft_calendar_credentials"/>
<field name="domain_force">[('user_ids', 'in', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_create" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_read" eval="1"/>
</record>
<record id="microsoft_calendar_token_system_access" model="ir.rule">
<field name="name">Microsoft Calendar manage all tokens</field>
<field name="model_id" ref="model_microsoft_calendar_credentials"/>
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
</record>
</data>
</odoo>
@@ -1,5 +1,5 @@
from datetime import datetime, timedelta
from unittest.mock import patch
from datetime import timedelta, datetime
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
@@ -299,3 +299,83 @@ class TestCreateEvents(TestCommon):
self.assertEqual(self.organizer_user._get_microsoft_sync_status(), "sync_paused")
self.assertTrue(record.need_sync_m, "Sync variable must be true for updating event when sync re-activates")
mock_insert.assert_not_called()
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'insert')
def test_sync_create_update_single_event(self, mock_insert, mock_get_events):
"""
If the synchronization with Outlook is stopped, then records (events and recurrences) created or updated
should not be synced. They must be synced only when created or updated having the synchronization active.
In this test, the synchronization is stopped and an event is created locally. After this, the synchronization
is restarted and the event is updated (this way, syncing it with Outlook Calendar).
"""
# Stop the synchronization for clearing the last_sync_date.
self.organizer_user.with_user(self.organizer_user).sudo().stop_microsoft_synchronization()
self.assertEqual(self.organizer_user.microsoft_last_sync_date, False,
"Variable last_sync_date must be False due to sync stop.")
# Create a not synced event (local).
simple_event_values_updated = self.simple_event_values
for date_field in ['start', 'stop']:
simple_event_values_updated[date_field] = simple_event_values_updated[date_field].replace(year=datetime.now().year)
event = self.env["calendar.event"].with_user(self.organizer_user).create(simple_event_values_updated)
# Assert that insert was not called and prepare mock for the synchronization restart.
mock_insert.assert_not_called()
mock_get_events.return_value = ([], None)
# Restart the synchronization with Outlook Calendar.
self.organizer_user.with_user(self.organizer_user).sudo().restart_microsoft_synchronization()
# Last_sync_date is manually updated here due to simulate real use in the test environment.
self.organizer_user.microsoft_calendar_account_id.last_sync_date = event.write_date + timedelta(minutes=10)
# Sync microsoft calendar, considering that ten minutes were passed after the event creation.
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
self.call_post_commit_hooks()
event.invalidate_recordset()
# Assert that insert function was not called and check last_sync_date variable value.
mock_insert.assert_not_called()
self.assertNotEqual(self.organizer_user.microsoft_last_sync_date, False,
"Variable last_sync_date must not be empty after sync.")
self.assertLessEqual(event.write_date, self.organizer_user.microsoft_last_sync_date,
"Event creation must happen before last_sync_date")
# Assert that the local event did not get synced after synchronization restart.
self.assertEqual(event.ms_organizer_event_id, False,
"Event should not be synchronized while sync is paused.")
self.assertEqual(event.ms_universal_event_id, False,
"Event should not be synchronized while sync is paused.")
# Synchronize after updating last_sync_date and assert that insert was not called.
# Last_sync_date is manually updated here due to simulate real use in the test environment.
self.organizer_user.microsoft_calendar_account_id.last_sync_date = event.write_date + timedelta(minutes=10)
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
self.call_post_commit_hooks()
mock_insert.assert_not_called()
# Update local event information.
event.write({
"name": "New event name"
})
self.call_post_commit_hooks()
# Prepare mock for new synchronization.
event_id = "123"
event_iCalUId = "456"
mock_insert.return_value = (event_id, event_iCalUId)
mock_get_events.return_value = ([], None)
# Synchronize local event with Outlook after updating it locally.
# Last_sync_date is manually updated here due to simulate real use in the test environment.
self.organizer_user.microsoft_calendar_account_id.last_sync_date = event.write_date - timedelta(minutes=10)
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
self.call_post_commit_hooks()
event.invalidate_recordset()
# Assert that the event got synchronized with Microsoft (through mock).
self.assertEqual(event.ms_organizer_event_id, "123")
self.assertEqual(event.ms_universal_event_id, "456")
# Assert that the Microsoft Insert was called once.
mock_insert.assert_called_once()
@@ -16,6 +16,7 @@
<field name="microsoft_calendar_token" string="User Token" readonly="1"/>
<field name="microsoft_calendar_token_validity" string="Token Validity" readonly="1"/>
<field name="microsoft_calendar_sync_token" string="Next Sync Token" readonly="1"/>
<field name="microsoft_last_sync_date" string="Last Sync Time" readonly="0"/>
<button
string="Reset Account" colspan="2" class="btn btn-secondary mt-3"
type="action" name="%(microsoft_calendar_reset_account_action)d"
@@ -50,6 +50,7 @@ class ResetMicrosoftAccount(models.TransientModel):
# We commit to make sure the _microsoft_delete are called when we still have a token on the user.
self.env.cr.commit()
self.user_id._set_microsoft_auth_tokens(False, False, 0)
self.user_id.write({
'microsoft_calendar_sync_token': False,
self.user_id.microsoft_calendar_account_id.write({
'calendar_sync_token': False,
'last_sync_date': False
})