From 24c2d7e9d609f02b1814be0faf82352bfeb45d96 Mon Sep 17 00:00:00 2001 From: Arnaud Joset Date: Wed, 30 Jun 2021 10:17:53 +0000 Subject: [PATCH] [IMP] google_calendar: move credentials from res_user table to a dedicated table Before this commit, several token related data were stored on the res_users table. During event sync, the table could be locked and this commit aims to move these data to a dedicated table to avoid bottleneck effets. Taskid: 2484335 Part-of: odoo/odoo#68700 --- addons/google_account/controllers/main.py | 2 +- addons/google_calendar/models/__init__.py | 1 + .../models/google_credentials.py | 117 ++++++++++++++++++ addons/google_calendar/models/res_users.py | 92 ++++---------- .../security/ir.model.access.csv | 1 + 5 files changed, 144 insertions(+), 69 deletions(-) create mode 100644 addons/google_calendar/models/google_credentials.py diff --git a/addons/google_account/controllers/main.py b/addons/google_account/controllers/main.py index 3483a9f7832..e34b1fb95cf 100644 --- a/addons/google_account/controllers/main.py +++ b/addons/google_account/controllers/main.py @@ -20,7 +20,7 @@ class GoogleAuth(http.Controller): if kw.get('code'): access_token, refresh_token, ttl = request.env['google.service']._get_google_tokens(kw['code'], service) # LUL TODO only defined in google_calendar - request.env.user._set_auth_tokens(access_token, refresh_token, ttl) + request.env.user.google_cal_account_id._set_auth_tokens(access_token, refresh_token, ttl) return request.redirect(url_return) elif kw.get('error'): return request.redirect("%s%s%s" % (url_return, "?error=", kw['error'])) diff --git a/addons/google_calendar/models/__init__.py b/addons/google_calendar/models/__init__.py index 38e162235e9..f3b7298adb6 100644 --- a/addons/google_calendar/models/__init__.py +++ b/addons/google_calendar/models/__init__.py @@ -7,3 +7,4 @@ from . import calendar from . import calendar_recurrence_rule from . import res_users from . import calendar_attendee +from . import google_credentials diff --git a/addons/google_calendar/models/google_credentials.py b/addons/google_calendar/models/google_credentials.py new file mode 100644 index 00000000000..8c0e4cf9962 --- /dev/null +++ b/addons/google_calendar/models/google_credentials.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import requests +from datetime import timedelta + +from odoo import fields, models, _ +from odoo.exceptions import UserError +from odoo.addons.google_account.models.google_service import GOOGLE_TOKEN_ENDPOINT +from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService, InvalidSyncToken +from odoo.addons.google_calendar.models.google_sync import google_calendar_token + +_logger = logging.getLogger(__name__) + +class GoogleCredentials(models.Model): + """"Google Account of res_users""" + + _name = 'google.calendar.credentials' + _description = 'Google Calendar Account Data' + + user_ids = fields.One2many('res.users', 'google_cal_account_id', required=True) + calendar_rtoken = fields.Char('Refresh Token', copy=False, groups="base.group_system") + calendar_token = fields.Char('User token', copy=False, groups="base.group_system") + calendar_token_validity = fields.Datetime('Token Validity', copy=False, groups="base.group_system") + calendar_sync_token = fields.Char('Next Sync Token', copy=False, groups="base.group_system") + calendar_cal_id = fields.Char('Calendar ID', copy=False, help='Last Calendar ID who has been synchronized. If it is changed, we remove all links between GoogleID and Odoo Google Internal ID') + synchronization_stopped = fields.Boolean('Google Synchronization stopped', copy=False) + + def _set_auth_tokens(self, access_token, refresh_token, ttl): + self.write({ + 'calendar_rtoken': refresh_token, + 'calendar_token': access_token, + 'calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl) if ttl else False, + }) + + def _google_calendar_authenticated(self): + self.ensure_one() + return bool(self.sudo().calendar_rtoken) + + def _is_google_calendar_valid(self): + self.ensure_one() + return self.calendar_token_validity and self.calendar_token_validity >= (fields.Datetime.now() + timedelta(minutes=1)) + + def _refresh_google_calendar_token(self): + # LUL TODO similar code exists in google_drive. Should be factorized in google_account + self.ensure_one() + get_param = self.env['ir.config_parameter'].sudo().get_param + client_id = get_param('google_calendar_client_id') + client_secret = get_param('google_calendar_client_secret') + + if not client_id or not client_secret: + raise UserError(_("The account for the Google Calendar service is not configured.")) + + headers = {"content-type": "application/x-www-form-urlencoded"} + data = { + 'refresh_token': self.calendar_rtoken, + 'client_id': client_id, + 'client_secret': client_secret, + 'grant_type': 'refresh_token', + } + + try: + _dummy, response, _dummy = self.env['google.service']._do_request(GOOGLE_TOKEN_ENDPOINT, params=data, headers=headers, method='POST', preuri='') + ttl = response.get('expires_in') + self.write({ + 'calendar_token': response.get('access_token'), + 'calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl), + }) + except requests.HTTPError as error: + if error.response.status_code in (400, 401): # invalid grant or invalid client + # Delete refresh token and make sure it's commited + self.env.cr.rollback() + self._set_auth_tokens(False, False, 0) + self.env.cr.commit() + error_key = error.response.json().get("error", "nc") + error_msg = _("An error occurred while generating the token. Your authorization code may be invalid or has already expired [%s]. " + "You should check your Client ID and secret on the Google APIs plateform or try to stop and restart your calendar synchronisation.", + error_key) + raise UserError(error_msg) + + def _sync_google_calendar(self, calendar_service: GoogleCalendarService): + self.ensure_one() + if self.synchronization_stopped: + return False + + # don't attempt to sync when another sync is already in progress, as we wouldn't be + # able to commit the transaction anyway (row is locked) + self.env.cr.execute("""SELECT id FROM res_users WHERE id = %s FOR NO KEY UPDATE SKIP LOCKED""", [self.id]) + if not self.env.cr.rowcount: + _logger.info("skipping calendar sync, locked user %s", self.login) + return False + + full_sync = not bool(self.calendar_sync_token) + with google_calendar_token(self) as token: + try: + events, next_sync_token, default_reminders = calendar_service.get_events(self.calendar_sync_token, token=token) + except InvalidSyncToken: + events, next_sync_token, default_reminders = calendar_service.get_events(token=token) + full_sync = True + self.calendar_sync_token = next_sync_token + + # Google -> Odoo + events.clear_type_ambiguity(self.env) + recurrences = events.filter(lambda e: e.is_recurrence()) + synced_recurrences = self.env['calendar.recurrence']._sync_google2odoo(recurrences) + synced_events = self.env['calendar.event']._sync_google2odoo(events - recurrences, default_reminders=default_reminders) + + # Odoo -> Google + recurrences = self.env['calendar.recurrence']._get_records_to_sync(full_sync=full_sync) + recurrences -= synced_recurrences + recurrences._sync_odoo2google(calendar_service) + synced_events |= recurrences.calendar_event_ids - recurrences._get_outliers() + events = self.env['calendar.event']._get_records_to_sync(full_sync=full_sync) + (events - synced_events)._sync_odoo2google(calendar_service) + + return bool(events | synced_events) or bool(recurrences | synced_recurrences) diff --git a/addons/google_calendar/models/res_users.py b/addons/google_calendar/models/res_users.py index b28e7f6d93e..e0c56916207 100644 --- a/addons/google_calendar/models/res_users.py +++ b/addons/google_calendar/models/res_users.py @@ -2,90 +2,44 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -import requests -from odoo.addons.google_calendar.models.google_sync import google_calendar_token -from datetime import timedelta -from odoo import api, fields, models, _ -from odoo.exceptions import UserError -from odoo.loglevels import exception_to_unicode -from odoo.addons.google_account.models.google_service import GOOGLE_TOKEN_ENDPOINT +from odoo import api, fields, models, Command from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService, InvalidSyncToken +from odoo.addons.google_calendar.models.google_sync import google_calendar_token +from odoo.loglevels import exception_to_unicode _logger = logging.getLogger(__name__) class User(models.Model): _inherit = 'res.users' - google_calendar_rtoken = fields.Char('Refresh Token', copy=False, groups="base.group_system") - google_calendar_token = fields.Char('User token', copy=False, groups="base.group_system") - google_calendar_token_validity = fields.Datetime('Token Validity', copy=False) - google_calendar_sync_token = fields.Char('Next Sync Token', copy=False) - google_calendar_cal_id = fields.Char('Calendar ID', copy=False, help='Last Calendar ID who has been synchronized. If it is changed, we remove all links between GoogleID and Odoo Google Internal ID') - google_synchronization_stopped = fields.Boolean('Google Synchronization stopped', copy=False) + google_cal_account_id = fields.Many2one('google.calendar.credentials') + google_calendar_rtoken = fields.Char(related='google_cal_account_id.calendar_rtoken', groups="base.group_system") + google_calendar_token = fields.Char(related='google_cal_account_id.calendar_token') + google_calendar_token_validity = fields.Datetime(related='google_cal_account_id.calendar_token_validity') + google_calendar_sync_token = fields.Char(related='google_cal_account_id.calendar_sync_token') + google_calendar_cal_id = fields.Char(related='google_cal_account_id.calendar_cal_id') + google_synchronization_stopped = fields.Boolean(related='google_cal_account_id.synchronization_stopped', readonly=False) + + _sql_constraints = [ + ('google_token_uniq', 'unique (google_cal_account_id)', "The user has already a google account"), + ] + @property def SELF_READABLE_FIELDS(self): - return super().SELF_READABLE_FIELDS + ['google_synchronization_stopped'] + return super().SELF_READABLE_FIELDS + ['google_synchronization_stopped', 'google_cal_account_id'] @property def SELF_WRITEABLE_FIELDS(self): - return super().SELF_WRITEABLE_FIELDS + ['google_synchronization_stopped'] - - def _set_auth_tokens(self, access_token, refresh_token, ttl): - self.write({ - 'google_calendar_rtoken': refresh_token, - 'google_calendar_token': access_token, - 'google_calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl) if ttl else False, - }) - - def _google_calendar_authenticated(self): - return bool(self.sudo().google_calendar_rtoken) + return super().SELF_WRITEABLE_FIELDS + ['google_synchronization_stopped', 'google_cal_account_id'] def _get_google_calendar_token(self): self.ensure_one() - if self.google_calendar_rtoken and not self._is_google_calendar_valid(): - self._refresh_google_calendar_token() - return self.google_calendar_token - - def _is_google_calendar_valid(self): - return self.google_calendar_token_validity and self.google_calendar_token_validity >= (fields.Datetime.now() + timedelta(minutes=1)) - - def _refresh_google_calendar_token(self): - # LUL TODO similar code exists in google_drive. Should be factorized in google_account - self.ensure_one() - get_param = self.env['ir.config_parameter'].sudo().get_param - client_id = get_param('google_calendar_client_id') - client_secret = get_param('google_calendar_client_secret') - - if not client_id or not client_secret: - raise UserError(_("The account for the Google Calendar service is not configured.")) - - headers = {"content-type": "application/x-www-form-urlencoded"} - data = { - 'refresh_token': self.google_calendar_rtoken, - 'client_id': client_id, - 'client_secret': client_secret, - 'grant_type': 'refresh_token', - } - - try: - dummy, response, dummy = self.env['google.service']._do_request(GOOGLE_TOKEN_ENDPOINT, params=data, headers=headers, method='POST', preuri='') - ttl = response.get('expires_in') - self.write({ - 'google_calendar_token': response.get('access_token'), - 'google_calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl), - }) - except requests.HTTPError as error: - if error.response.status_code in (400, 401): # invalid grant or invalid client - # Delete refresh token and make sure it's commited - self.env.cr.rollback() - self._set_auth_tokens(False, False, 0) - self.env.cr.commit() - error_key = error.response.json().get("error", "nc") - error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired [%s]", error_key) - raise UserError(error_msg) + if self.google_cal_account_id.calendar_rtoken and not self.google_cal_account_id._is_google_calendar_valid(): + self.sudo().google_cal_account_id._refresh_google_calendar_token() + return self.google_cal_account_id.calendar_token def _sync_google_calendar(self, calendar_service: GoogleCalendarService): self.ensure_one() @@ -102,11 +56,11 @@ class User(models.Model): full_sync = not bool(self.google_calendar_sync_token) with google_calendar_token(self) as token: try: - events, next_sync_token, default_reminders = calendar_service.get_events(self.google_calendar_sync_token, token=token) + events, next_sync_token, default_reminders = calendar_service.get_events(self.google_cal_account_id.calendar_sync_token, token=token) except InvalidSyncToken: events, next_sync_token, default_reminders = calendar_service.get_events(token=token) full_sync = True - self.google_calendar_sync_token = next_sync_token + self.google_cal_account_id.calendar_sync_token = next_sync_token # Google -> Odoo events.clear_type_ambiguity(self.env) @@ -144,6 +98,8 @@ class User(models.Model): def restart_google_synchronization(self): self.ensure_one() + if not self.google_cal_account_id: + self.google_cal_account_id = self.env['google.calendar.credentials'].sudo().create([{'user_ids': [Command.set(self.ids)]}]) self.google_synchronization_stopped = False self.env['calendar.recurrence']._restart_google_sync() self.env['calendar.event']._restart_google_sync() diff --git a/addons/google_calendar/security/ir.model.access.csv b/addons/google_calendar/security/ir.model.access.csv index 2a46b84b501..ccbc397081c 100644 --- a/addons/google_calendar/security/ir.model.access.csv +++ b/addons/google_calendar/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink google_calendar_account_reset,google_calendar_account_reset_access_right,model_google_calendar_account_reset,base.group_system,1,1,1,0 +google_calendar.access_google_calendar_credentials,access_google_calendar_credentials,google_calendar.model_google_calendar_credentials,base.group_user,1,0,0,0