[FIX] google_calendar: do not spam while updating recurrent events

Before this task, lots of mails were sent after updating or deleting recurrent event in 'All events' or 'This and future events' update type. This was happening because updating these recurrent events was triggering patch calls event by event, when they should be handled in batch.

After this commit, updating or deleting recurrent events should trigger at most two mails for Google users.

closes odoo/odoo#137607

Task-id: 3163695
X-original-commit: ad2106babda61446bce13283d570dc723418b630
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-10-05 15:36:25 +00:00
parent 11a0f8fab7
commit a0d4c62108
8 changed files with 222 additions and 52 deletions
+38
View File
@@ -9,6 +9,7 @@ from uuid import uuid4
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
class Meeting(models.Model):
_name = 'calendar.event'
@@ -60,6 +61,31 @@ class Meeting(models.Model):
for vals in vals_list
])
@api.model
def _check_values_to_sync(self, values):
""" Return True if values being updated intersects with Google synced values and False otherwise. """
synced_fields = self._get_google_synced_fields()
values_to_sync = any(key in synced_fields for key in values)
return values_to_sync
@api.model
def _get_update_future_events_values(self):
""" Add parameters for updating events within the _update_future_events function scope. """
update_future_events_values = super()._get_update_future_events_values()
return {**update_future_events_values, 'need_sync': False}
@api.model
def _get_remove_sync_id_values(self):
""" Add parameters for removing event synchronization while updating the events in super class. """
remove_sync_id_values = super()._get_remove_sync_id_values()
return {**remove_sync_id_values, 'google_id': False}
@api.model
def _get_archive_values(self):
""" Return the parameters for archiving events. Do not synchronize events after archiving. """
archive_values = super()._get_archive_values()
return {**archive_values, 'need_sync': False}
def write(self, values):
recurrence_update_setting = values.get('recurrence_update')
if recurrence_update_setting in ('all_events', 'future_events') and len(self) == 1:
@@ -233,6 +259,18 @@ class Meeting(models.Model):
commands += [(0, 0, {'duration': duration, 'interval': interval, 'name': name, 'alarm_type': alarm_type})]
return commands
def action_mass_archive(self, recurrence_update_setting):
""" Delete recurrence in Odoo if in 'all_events' or in 'future_events' edge case, triggering one mail. """
self.ensure_one()
google_service = GoogleCalendarService(self.env['google.service'])
archive_future_events = recurrence_update_setting == 'future_events' and self == self.recurrence_id.base_event_id
if recurrence_update_setting == 'all_events' or archive_future_events:
self.recurrence_id.with_context(is_recurrence=True)._google_delete(google_service, self.recurrence_id.google_id)
# Increase performance handling 'future_events' edge case as it was an 'all_events' update.
if archive_future_events:
recurrence_update_setting = 'all_events'
super(Meeting, self).action_mass_archive(recurrence_update_setting)
def _google_values(self):
if self.allday:
start = {'date': self.start_date.isoformat()}
@@ -64,8 +64,8 @@ class RecurrenceRule(models.Model):
def _write_events(self, values, dtstart=None):
values.pop('google_id', False)
# If only some events are updated, sync those events.
values['need_sync'] = bool(dtstart)
# Events will be updated by patch requests, do not sync events for avoiding spam.
values['need_sync'] = False
return super()._write_events(values, dtstart=dtstart)
def _cancel(self):
+4 -1
View File
@@ -242,6 +242,8 @@ class GoogleSync(models.AbstractModel):
def _google_delete(self, google_service: GoogleCalendarService, google_id, timeout=TIMEOUT):
with google_calendar_token(self.env.user.sudo()) as token:
if token:
is_recurrence = self._context.get('is_recurrence', False)
google_service.google_service = google_service.google_service.with_context(is_recurrence=is_recurrence)
google_service.delete(google_id, token=token, timeout=timeout)
# When the record has been deleted on our side, we need to delete it on google but we don't want
# to raise an error because the record don't exists anymore.
@@ -256,7 +258,8 @@ class GoogleSync(models.AbstractModel):
except HTTPError as e:
if e.response.status_code in (400, 403):
self._google_error_handling(e)
self.exists().with_context(dont_notify=True).need_sync = False
if values:
self.exists().with_context(dont_notify=True).need_sync = False
@after_commit
def _google_insert(self, google_service: GoogleCalendarService, values, timeout=TIMEOUT):
@@ -106,7 +106,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
})
partner_model = self.env.ref('base.model_res_partner')
partner = self.env['res.partner'].search([], limit=1)
with self.assertQueryCount(__system__=84):
with self.assertQueryCount(__system__=86):
event = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
@@ -326,8 +326,8 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'name': 'New name',
'recurrence_update': 'future_events',
})
self.assertGoogleEventPatched(event.google_id, {
'id': event.google_id,
self.assertGoogleEventInserted({
'id': False,
'start': {'date': str(event.start_date)},
'end': {'date': str(event.stop_date + relativedelta(days=1))},
'summary': 'New name',
@@ -336,9 +336,10 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'guestsCanModify': True,
'organizer': {'email': 'odoobot@example.com', 'self': True},
'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}},
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.recurrence_id.id}},
'reminders': {'overrides': [], 'useDefault': False},
'visibility': 'public',
'recurrence': ['RRULE:FREQ=WEEKLY;WKST=SU;COUNT=1;BYDAY=WE']
}, timeout=3)
@patch_api
@@ -412,8 +413,9 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'name': 'New name',
'recurrence_update': 'all_events',
})
self.assertGoogleEventPatched(recurrence.google_id, {
'id': recurrence.google_id,
new_recurrence = self.env['calendar.recurrence'].search([('id', '>', recurrence.id)])
self.assertGoogleEventInserted({
'id': False,
'start': {'date': str(event.start_date)},
'end': {'date': str(event.stop_date + relativedelta(days=1))},
'summary': 'New name',
@@ -422,8 +424,8 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'guestsCanModify': True,
'organizer': {'email': 'odoobot@example.com', 'self': True},
'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: recurrence.id}},
'recurrence': ['RRULE:FREQ=WEEKLY;WKST=SU;COUNT=2;BYDAY=WE'],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: new_recurrence.id}},
'reminders': {'overrides': [], 'useDefault': False},
'visibility': 'public',
}, timeout=3)
@@ -578,8 +580,9 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'name': 'New name',
'recurrence_update': 'all_events',
})
self.assertGoogleEventPatched(recurrence.google_id, {
'id': recurrence.google_id,
new_recurrence = self.env['calendar.recurrence'].search([('id', '>', recurrence.id)])
self.assertGoogleEventInserted({
'id': False,
'start': {'dateTime': "2020-01-15T08:00:00+00:00", 'timeZone': 'Europe/Brussels'},
'end': {'dateTime': "2020-01-15T09:00:00+00:00", 'timeZone': 'Europe/Brussels'},
'summary': 'New name',
@@ -588,8 +591,8 @@ class TestSyncOdoo2Google(TestSyncGoogle):
'guestsCanModify': True,
'organizer': {'email': 'odoobot@example.com', 'self': True},
'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: recurrence.id}},
'recurrence': ['RRULE:FREQ=WEEKLY;WKST=SU;COUNT=2;BYDAY=WE'],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: new_recurrence.id}},
'reminders': {'overrides': [], 'useDefault': False},
'visibility': 'public',
}, timeout=3)
@@ -92,6 +92,12 @@ class GoogleCalendarService():
url = "/calendar/v3/calendars/primary/events/%s?sendUpdates=all" % event_id
headers = {'Content-type': 'application/json'}
params = {'access_token': token}
# Delete all events from recurrence in a single request to Google and triggering a single mail.
# The 'singleEvents' parameter is a trick that tells Google API to delete all recurrent events individually,
# making the deletion be handled entirely on their side, and then we archive the events in Odoo.
is_recurrence = self.google_service._context.get('is_recurrence', True)
if is_recurrence:
params['singleEvents'] = 'true'
try:
self.google_service._do_request(url, params, headers=headers, method='DELETE', timeout=timeout)
except requests.HTTPError as e: