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