From f6c03b7097778bea205d05c68e910efbd46918f6 Mon Sep 17 00:00:00 2001 From: fw-bot Date: Tue, 27 Apr 2021 09:39:53 +0000 Subject: [PATCH] [FIX] calendar: improve calendar recurrency creation performances Turn off calendar.attendees notification during synced_events creation to improve performances. The calendar notification are only computed once after all the synced events have been created. Add a flag in google_calendar.js to avoid retriggering synchronization while navigating the calendar view if the past synchronization is still pending. opw: 2450259 + fixes/adaptations to 14.3 branch FW of https://github.com/odoo/odoo/pull/69619 closes odoo/odoo#70035 X-original-commit: 28ccb2b88a7793058c81aabffb43071165886f40 Signed-off-by: Yannick Tivisse (yti) Signed-off-by: Arnaud Joset --- addons/calendar/models/calendar_event.py | 21 +++--- addons/calendar/models/calendar_recurrence.py | 2 +- addons/google_calendar/models/calendar.py | 6 +- .../models/calendar_recurrence_rule.py | 13 ++-- addons/google_calendar/models/google_sync.py | 4 +- .../static/src/js/google_calendar.js | 8 +++ .../tests/test_sync_google2odoo.py | 65 +++++++++++++++++++ .../tests/test_sync_odoo2google.py | 7 +- 8 files changed, 102 insertions(+), 24 deletions(-) diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py index a102a3da5c9..baf32a001bb 100644 --- a/addons/calendar/models/calendar_event.py +++ b/addons/calendar/models/calendar_event.py @@ -389,11 +389,12 @@ class Meeting(models.Model): events = super().create(other_vals) for vals in recurring_vals: - - recurrence_values = {field: vals.pop(field) for field in recurrence_fields if field in vals} vals['follow_recurrence'] = True - event = super().create(vals) - events |= event + recurring_events = super().create(recurring_vals) + events += recurring_events + + for event, vals in zip(recurring_events, recurring_vals): + recurrence_values = {field: vals.pop(field) for field in recurrence_fields if field in vals} if vals.get('recurrency'): detached_events = event._apply_recurrence_values(recurrence_values) detached_events.active = False @@ -402,8 +403,8 @@ class Meeting(models.Model): self.env.ref('calendar.calendar_template_meeting_invitation', raise_if_not_found=False) ) events._sync_activities(fields={f for vals in vals_list for f in vals.keys()}) - - events._setup_alarms() + if not self.env.context.get('dont_notify'): + events._setup_alarms() return events @@ -488,8 +489,8 @@ class Meeting(models.Model): (detached_events & self).active = False (detached_events - self).with_context(archive_on_error=True).unlink() - - self._setup_alarms() + if not self.env.context.get('dont_notify'): + self._setup_alarms() current_attendees = self.filtered('active').attendee_ids if 'partner_ids' in values: @@ -656,7 +657,9 @@ class Meeting(models.Model): # Don't trigger for past alarms, they would be skipped by design cron._trigger(at=at) if any(alarm.alarm_type == 'notification' for alarm in event.alarm_ids): - alarm_manager._notify_next_alarm(event.partner_ids.ids) + # filter events before notifying attendees through calendar_alarm_manager + need_notifs = event.filtered(lambda ev: ev.alarm_ids and ev.stop >= fields.Datetime.now()) + alarm_manager._notify_next_alarm(need_notifs.partner_ids.ids) # ------------------------------------------------------------ # RECURRENCY diff --git a/addons/calendar/models/calendar_recurrence.py b/addons/calendar/models/calendar_recurrence.py index 07b370ca7b0..3895886f067 100644 --- a/addons/calendar/models/calendar_recurrence.py +++ b/addons/calendar/models/calendar_recurrence.py @@ -292,7 +292,7 @@ class RecurrenceRule(models.Model): :param dstart: if provided, only write events starting from this point in time """ events = self._get_events_from(dtstart) if dtstart else self.calendar_event_ids - return events.with_context(no_mail_to_attendees=True).write(dict(values, recurrence_update='self_only')) + return events.with_context(no_mail_to_attendees=True, dont_notify=True).write(dict(values, recurrence_update='self_only')) def _rrule_serialize(self): """ diff --git a/addons/google_calendar/models/calendar.py b/addons/google_calendar/models/calendar.py index d506ca28ba0..1574659ee6c 100644 --- a/addons/google_calendar/models/calendar.py +++ b/addons/google_calendar/models/calendar.py @@ -41,7 +41,8 @@ class Meeting(models.Model): @api.model_create_multi def create(self, vals_list): - return super().create([ + notify_context = self.env.context.get('dont_notify', False) + return super(Meeting, self.with_context(dont_notify=notify_context)).create([ dict(vals, need_sync=False) if vals.get('recurrence_id') or vals.get('recurrency') else vals for vals in vals_list ]) @@ -50,7 +51,8 @@ class Meeting(models.Model): recurrence_update_setting = values.get('recurrence_update') if recurrence_update_setting in ('all_events', 'future_events') and len(self) == 1: values = dict(values, need_sync=False) - res = super().write(values) + notify_context = self.env.context.get('dont_notify', False) + res = super(Meeting, self.with_context(dont_notify=notify_context)).write(values) if recurrence_update_setting in ('all_events',) and len(self) == 1 and values.keys() & self._get_google_synced_fields(): self.recurrence_id.need_sync = True return res diff --git a/addons/google_calendar/models/calendar_recurrence_rule.py b/addons/google_calendar/models/calendar_recurrence_rule.py index 40d254d2be3..1924268f5ce 100644 --- a/addons/google_calendar/models/calendar_recurrence_rule.py +++ b/addons/google_calendar/models/calendar_recurrence_rule.py @@ -2,8 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import re - -from odoo import api, fields, models +from odoo import api, models from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService @@ -137,12 +136,16 @@ class RecurrenceRule(models.Model): vals['calendar_event_ids'] = [(4, base_event.id)] # event_tz is written on event in Google but on recurrence in Odoo vals['event_tz'] = gevent.start.get('timeZone') - recurrence = super()._create_from_google(gevents, vals_list) - recurrence._apply_recurrence() + recurrence = super(RecurrenceRule, self.with_context(dont_notify=True))._create_from_google(gevents, vals_list) + recurrence.with_context(dont_notify=True)._apply_recurrence() return recurrence def _get_sync_domain(self): - return [('calendar_event_ids.user_id', '=', self.env.user.id)] + # Empty rrule may exists in historical data. It is not a desired behavior but it could have been created with + # older versions of the module. When synced, these recurrency may come back from Google after database cleaning + # and trigger errors as the records are not properly populated. + # We also prevent sync of other user recurrent events. + return [('calendar_event_ids.user_id', '=', self.env.user.id), ('rrule', '!=', False)] @api.model def _odoo_values(self, google_recurrence, default_reminders=()): diff --git a/addons/google_calendar/models/google_sync.py b/addons/google_calendar/models/google_sync.py index f228bbf2724..5d96f055fc5 100644 --- a/addons/google_calendar/models/google_sync.py +++ b/addons/google_calendar/models/google_sync.py @@ -148,7 +148,7 @@ class GoogleSync(models.AbstractModel): dict(self._odoo_values(e, default_reminders), need_sync=False) for e in new ] - new_odoo = self._create_from_google(new, odoo_values) + new_odoo = self.with_context(dont_notify=True)._create_from_google(new, odoo_values) cancelled = existing.cancelled() cancelled_odoo = self.browse(cancelled.odoo_ids(self.env)) cancelled_odoo._cancel() @@ -161,7 +161,7 @@ class GoogleSync(models.AbstractModel): # Migration from 13.4 does not fill write_date. Therefore, we force the update from Google. if not odoo_record.write_date or updated >= pytz.utc.localize(odoo_record.write_date): vals = dict(self._odoo_values(gevent, default_reminders), need_sync=False) - odoo_record._write_from_google(gevent, vals) + odoo_record.with_context(dont_notify=True)._write_from_google(gevent, vals) synced_records |= odoo_record return synced_records diff --git a/addons/google_calendar/static/src/js/google_calendar.js b/addons/google_calendar/static/src/js/google_calendar.js index 3712dbe6e86..167917d51b0 100644 --- a/addons/google_calendar/static/src/js/google_calendar.js +++ b/addons/google_calendar/static/src/js/google_calendar.js @@ -21,6 +21,7 @@ const GoogleCalendarModel = CalendarModel.include({ init: function () { this._super.apply(this, arguments); this.google_is_sync = true; + this.google_pending_sync = false; }, /** @@ -39,6 +40,10 @@ const GoogleCalendarModel = CalendarModel.include({ */ async _loadCalendar() { const _super = this._super.bind(this); + // When the calendar synchronization takes some time, prevents retriggering the sync while navigating the calendar. + if (this.google_pending_sync) { + return _super(...arguments); + } try { await Promise.race([ new Promise(resolve => setTimeout(resolve, 1000)), @@ -49,6 +54,7 @@ const GoogleCalendarModel = CalendarModel.include({ error.event.preventDefault(); } console.error("Could not synchronize Google events now.", error); + this.google_pending_sync = false; } return _super(...arguments); }, @@ -56,6 +62,7 @@ const GoogleCalendarModel = CalendarModel.include({ _syncGoogleCalendar(shadow = false) { var self = this; var context = this.getSession().user_context; + this.google_pending_sync = true; return this._rpc({ route: '/google_calendar/sync_data', params: { @@ -69,6 +76,7 @@ const GoogleCalendarModel = CalendarModel.include({ } else if (result.status === "no_new_event_from_google" || result.status === "need_refresh") { self.google_is_sync = true; } + self.google_pending_sync = false; return result }); }, diff --git a/addons/google_calendar/tests/test_sync_google2odoo.py b/addons/google_calendar/tests/test_sync_google2odoo.py index 8490b3b4271..98bb12dd566 100644 --- a/addons/google_calendar/tests/test_sync_google2odoo.py +++ b/addons/google_calendar/tests/test_sync_google2odoo.py @@ -681,3 +681,68 @@ class TestSyncGoogle2Odoo(TestSyncGoogle): event = self.env['calendar.event'].search([('google_id', '=', values.get('id'))]) self.assertFalse(event.exists(), "The old event should not exits anymore") self.assertGoogleAPINotCalled() + + @patch_api + def test_new_google_notifications(self): + """ Event from Google should not create notifications and trigger. It ruins the perfs on large databases """ + cron_id = self.env.ref('calendar.ir_cron_scheduler_alarm').id + triggers_before = self.env['ir.cron.trigger'].search([('cron_id', '=', cron_id)]) + google_id = 'oj44nep1ldf8a3ll02uip0c9aa' + start = datetime.today() + relativedelta(months=1, day=1, hours=1) + end = datetime.today() + relativedelta(months=1, day=1, hours=2) + updated = datetime.today() + relativedelta(minutes=1) + values = { + 'id': google_id, + 'description': 'Small mini desc', + 'organizer': {'email': 'odoocalendarref@gmail.com', 'self': True}, + 'summary': 'Pricing new update', + 'visibility': 'public', + 'attendees': [{ + 'displayName': 'Mitchell Admin', + 'email': 'admin@yourcompany.example.com', + 'responseStatus': 'needsAction' + }, ], + 'reminders': {'overrides': [{"method": "email", "minutes": 10}], 'useDefault': False}, + 'start': { + 'dateTime': pytz.utc.localize(start).isoformat(), + 'timeZone': 'Europe/Brussels' + }, + 'end': { + 'dateTime': pytz.utc.localize(end).isoformat(), + 'timeZone': 'Europe/Brussels' + }, + } + self.env['calendar.event']._sync_google2odoo(GoogleEvent([values])) + triggers_after = self.env['ir.cron.trigger'].search([('cron_id', '=', cron_id)]) + new_triggers = triggers_after - triggers_before + self.assertFalse(new_triggers, "The event should not be created with triggers.") + + # Event was created from Google and now it will be Updated from Google. + # No further notifications should be created. + values = { + 'id': google_id, + 'updated': pytz.utc.localize(updated).isoformat(), + 'description': 'New Super description', + 'organizer': {'email': 'odoocalendarref@gmail.com', 'self': True}, + 'summary': 'Pricing was not good, now it is correct', + 'visibility': 'public', + 'attendees': [{ + 'displayName': 'Mitchell Admin', + 'email': 'admin@yourcompany.example.com', + 'responseStatus': 'needsAction' + }, ], + 'reminders': {'overrides': [{"method": "email", "minutes": 10}], 'useDefault': False}, + 'start': { + 'dateTime': pytz.utc.localize(start).isoformat(), + 'timeZone': 'Europe/Brussels' + }, + 'end': { + 'dateTime': pytz.utc.localize(end).isoformat(), + 'timeZone': 'Europe/Brussels' + }, + } + self.env['calendar.event']._sync_google2odoo(GoogleEvent([values])) + triggers_after = self.env['ir.cron.trigger'].search([('cron_id', '=', cron_id)]) + new_triggers = triggers_after - triggers_before + self.assertFalse(new_triggers, "The event should not be created with triggers.") + self.assertGoogleAPINotCalled() diff --git a/addons/google_calendar/tests/test_sync_odoo2google.py b/addons/google_calendar/tests/test_sync_odoo2google.py index 7feafb24fec..f9a8c64e8f8 100644 --- a/addons/google_calendar/tests/test_sync_odoo2google.py +++ b/addons/google_calendar/tests/test_sync_odoo2google.py @@ -1,15 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime, date +from datetime import datetime from dateutil.relativedelta import relativedelta -from unittest.mock import MagicMock, patch +from unittest.mock import patch from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService from odoo.addons.google_calendar.models.res_users import User -from odoo.addons.google_calendar.models.google_sync import GoogleSync -from odoo.modules.registry import Registry -from odoo.addons.google_account.models.google_service import TIMEOUT from odoo.addons.google_calendar.tests.test_sync_common import TestSyncGoogle, patch_api