[FIX] google_calendar: updating recurrent events on google side

Before this commit when accepting a recurrent event from google calendar with option "this event", it didn't reflect on odoo calendar.
This happened due to the write_date check which applies google update only if their write_date is after odoo write_date,
but multiple updates from google might change some events write_date to now, which causes other google updates to get discarded.

This commit aims to fix this issue by keeping the write_date of the affected events before applying any google updates, and considering these dates instead of the live odoo write_date.

closes odoo/odoo#160357

Task: 3731552
X-original-commit: 3bcf6c458a4b483af6467d1c027b60771d1ce08d
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
ahmad(alah)
2024-04-04 11:48:12 +00:00
parent e2c6b088f0
commit f31174e021
4 changed files with 270 additions and 6 deletions
+4 -1
View File
@@ -154,6 +154,7 @@ class GoogleSync(models.AbstractModel):
"""
existing = google_events.exists(self.env)
new = google_events - existing - google_events.cancelled()
write_dates = self._context.get('write_dates', {})
odoo_values = [
dict(self._odoo_values(e, default_reminders), need_sync=False)
@@ -181,8 +182,10 @@ class GoogleSync(models.AbstractModel):
# This could be dangerous if google server time and odoo server time are different
updated = parse(gevent.updated)
odoo_record = self.browse(gevent.odoo_id(self.env))
# Use the record's write_date to apply Google updates only if they are newer than Odoo's write_date.
odoo_record_write_date = write_dates.get(odoo_record.id, odoo_record.write_date)
# 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):
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.with_context(dont_notify=True)._write_from_google(gevent, vals)
synced_records |= odoo_record
+11 -2
View File
@@ -61,8 +61,17 @@ class User(models.Model):
send_updates = not full_sync
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)
# We apply Google updates only if their write date is later than the write date in Odoo.
# It's possible that multiple updates affect the same record, maybe not directly.
# To handle this, we preserve the write dates in Odoo before applying any updates,
# and use these dates instead of the current live dates.
odoo_events = self.env['calendar.event'].browse((events - recurrences).odoo_ids(self.env))
odoo_recurrences = self.env['calendar.recurrence'].browse(recurrences.odoo_ids(self.env))
recurrences_write_dates = {r.id: r.write_date for r in odoo_recurrences}
events_write_dates = {e.id: e.write_date for e in odoo_events}
synced_recurrences = self.env['calendar.recurrence'].with_context(write_dates=recurrences_write_dates)._sync_google2odoo(recurrences)
synced_events = self.env['calendar.event'].with_context(write_dates=events_write_dates)._sync_google2odoo(events - recurrences, default_reminders=default_reminders)
# Odoo -> Google
recurrences = self.env['calendar.recurrence']._get_records_to_sync(full_sync=full_sync)
@@ -7,7 +7,10 @@ from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarServ
from odoo.addons.google_account.models.google_service import GoogleService
from odoo.addons.google_calendar.models.res_users import User
from odoo.addons.google_calendar.models.google_sync import GoogleSync
from odoo.tests.common import HttpCase
from odoo.tests.common import HttpCase, new_test_user
from freezegun import freeze_time
from contextlib import contextmanager
def patch_api(func):
@patch.object(GoogleSync, '_google_insert', MagicMock(spec=GoogleSync._google_insert))
@@ -24,6 +27,19 @@ class TestSyncGoogle(HttpCase):
super().setUp()
self.google_service = GoogleCalendarService(self.env['google.service'])
self.env.user.sudo().unpause_google_synchronization()
self.organizer_user = new_test_user(self.env, login="organizer_user")
self.attendee_user = new_test_user(self.env, login='attendee_user')
@contextmanager
def mock_datetime_and_now(self, mock_dt):
"""
Used when synchronization date (using env.cr.now()) is important
in addition to standard datetime mocks. Used mainly to detect sync
issues.
"""
with freeze_time(mock_dt), \
patch.object(self.env.cr, 'now', lambda: mock_dt):
yield
def assertGoogleEventDeleted(self, google_id):
GoogleSync._google_delete.assert_called()
@@ -2,14 +2,15 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import datetime, date
from datetime import datetime, date, timedelta
from dateutil.relativedelta import relativedelta
from odoo.tests.common import new_test_user
from odoo.exceptions import ValidationError
from odoo.addons.google_calendar.tests.test_sync_common import TestSyncGoogle, patch_api
from odoo.addons.google_calendar.utils.google_calendar import GoogleEvent
from odoo.addons.google_calendar.utils.google_calendar import GoogleEvent, GoogleCalendarService
from odoo import Command, tools
from unittest.mock import patch
class TestSyncGoogle2Odoo(TestSyncGoogle):
@@ -31,6 +32,79 @@ class TestSyncGoogle2Odoo(TestSyncGoogle):
'company_id': self.other_company.id,
})
def generate_recurring_event(self, mock_dt, **values):
""" Function Used to return a recurrence created at fake time of 'mock_dt'. """
rrule = values.pop('rrule', None)
google_id = values.pop('google_id', None)
with self.mock_datetime_and_now(mock_dt):
base_event = self.env['calendar.event'].with_user(self.organizer_user).create({
'name': 'coucou',
'need_sync': False,
**values
})
recurrence = self.env['calendar.recurrence'].with_user(self.organizer_user).create({
'google_id': google_id,
'rrule': rrule,
'need_sync': False,
'base_event_id': base_event.id,
})
recurrence._apply_recurrence()
return recurrence
def google_respond_to_recurrent_event_with_option_this_event(self, recurrence, event_index, response_status):
"""
Function returns google api response simulating 'self.attendee_user' responding to the event number 'event_index' (0-indexed)
in 'recurrence' with response of 'response_status' and option "This event"
"""
update_time = (recurrence.write_date + timedelta(seconds=30)).strftime("%Y-%m-%dT%H:%M:%SZ")
recurrence_google_values = recurrence._google_values()
recurrence_google_values['updated'] = update_time
updated_event_google_values = recurrence.calendar_event_ids.sorted('start')[event_index]._google_values()
updated_event_google_values['updated'] = update_time
updated_event_google_values['attendees'] = [
{"email": self.organizer_user.partner_id.email, "responseStatus": "accepted"},
{"email": self.attendee_user.partner_id.email, "responseStatus": response_status},
]
return [
recurrence_google_values,
updated_event_google_values,
]
def google_respond_to_recurrent_event_with_option_all_events(self, recurrence, response_status):
"""
Function returns google api response simulating 'self.attendee_user' responding to 'recurrence'
with response of 'response_status' and option "All events"
"""
update_time = (recurrence.write_date + timedelta(seconds=30)).strftime("%Y-%m-%dT%H:%M:%SZ")
recurrence_google_values = recurrence._google_values()
recurrence_google_values['updated'] = update_time
recurrence_google_values['attendees'] = [
{"email": self.organizer_user.partner_id.email, "responseStatus": "accepted"},
{"email": self.attendee_user.partner_id.email, "responseStatus": response_status},
]
return [recurrence_google_values]
def google_respond_to_recurrent_event_with_option_following_events(self, recurrence, event_index, response_status, rrule1, rrule2):
"""
Function returns google api response simulating 'self.attendee_user' responding to the event number 'event_index' (0-indexed)
in 'recurrence' with response of 'response_status' and option "This and following events".
"""
update_time = (recurrence.write_date + timedelta(seconds=30)).strftime("%Y-%m-%dT%H:%M:%SZ")
recurrence_google_values = recurrence._google_values()
recurrence_google_values['updated'] = update_time
updated_event_google_values = recurrence.calendar_event_ids.sorted('start')[event_index]._google_values()
updated_event_google_values['updated'] = update_time
updated_event_google_values['attendees'] = [
{"email": self.organizer_user.partner_id.email, "responseStatus": "accepted"},
{"email": self.attendee_user.partner_id.email, "responseStatus": response_status},
]
recurrence_google_values["recurrence"] = [rrule1]
updated_event_google_values["recurrence"] = [rrule2]
return [
recurrence_google_values,
updated_event_google_values,
]
@property
def now(self):
return pytz.utc.localize(datetime.now()).isoformat()
@@ -1872,3 +1946,165 @@ class TestSyncGoogle2Odoo(TestSyncGoogle):
self.assertEqual(len(events), 3, "it should have created a recurrence with 3 events")
self.assertEqual(events[0].attendee_ids[0].state, 'accepted', 'after google sync, organizer should have accepted status still')
self.assertGoogleAPINotCalled()
@patch.object(GoogleCalendarService, 'get_events')
def test_accepting_recurrent_event_with_this_event_option_synced_by_attendee(self, mock_get_events):
"""
Test accepting a recurring event with the option "This event" on Google Calendar and syncing the attendee's calendar.
Ensure that event is accepeted by attendee in Odoo.
"""
recurrence_id = "abcd1"
recurrence = self.generate_recurring_event(
mock_dt="2024-04-20",
google_id=recurrence_id,
rrule="FREQ=DAILY;INTERVAL=1;COUNT=4",
start=datetime(2024, 3, 20, 9, 0),
stop=datetime(2024, 3, 20, 10, 0),
partner_ids=[Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])],
)
google_events = self.google_respond_to_recurrent_event_with_option_this_event(recurrence, 2, 'accepted')
mock_get_events.return_value = (
GoogleEvent(google_events), None, [{'method': 'popup', 'minutes': 30}]
)
expected_states = ["needsAction", "needsAction", "accepted", "needsAction"]
with self.mock_datetime_and_now("2024-04-22"):
self.attendee_user.sudo()._sync_google_calendar(self.google_service)
for i, event in enumerate(recurrence.calendar_event_ids.sorted("start")):
self.assertEqual(event.attendee_ids[1].state, expected_states[i])
@patch.object(GoogleCalendarService, 'get_events')
def test_accepting_recurrent_event_with_this_event_option_synced_by_organizer(self, mock_get_events):
"""
Test accepting a recurring event with the option "This event" on Google Calendar and syncing the organizer's calendar.
Ensure that event is accepeted by attendee in Odoo.
"""
recurrence_id = "abcd2"
recurrence = self.generate_recurring_event(
mock_dt="2024-04-20",
google_id=recurrence_id,
rrule="FREQ=DAILY;INTERVAL=1;COUNT=4",
start=datetime(2024, 3, 20, 9, 0),
stop=datetime(2024, 3, 20, 10, 0),
partner_ids=[Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])],
)
google_events = self.google_respond_to_recurrent_event_with_option_this_event(recurrence, 2, 'accepted')
mock_get_events.return_value = (
GoogleEvent(google_events), None, [{'method': 'popup', 'minutes': 30}]
)
expected_states = ["needsAction", "needsAction", "accepted", "needsAction"]
with self.mock_datetime_and_now("2024-04-22"):
self.organizer_user.sudo()._sync_google_calendar(self.google_service)
for i, event in enumerate(recurrence.calendar_event_ids.sorted("start")):
self.assertEqual(event.attendee_ids[1].state, expected_states[i])
@patch.object(GoogleCalendarService, 'get_events')
def test_accepting_recurrent_event_with_all_events_option_synced_by_attendee(self, mock_get_events):
"""
Test accepting a recurring event with the option "All events" on Google Calendar and syncing the attendee's calendar.
Ensure that all events are accepeted by attendee in Odoo.
"""
recurrence_id = "abcd3"
recurrence = self.generate_recurring_event(
mock_dt="2024-04-20",
google_id=recurrence_id,
rrule="FREQ=DAILY;INTERVAL=1;COUNT=4",
start=datetime(2024, 3, 20, 9, 0),
stop=datetime(2024, 3, 20, 10, 0),
partner_ids=[Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])],
)
google_events = self.google_respond_to_recurrent_event_with_option_all_events(recurrence, "accepted")
mock_get_events.return_value = (
GoogleEvent(google_events), None, [{'method': 'popup', 'minutes': 30}]
)
expected_states = ["accepted", "accepted", "accepted", "accepted"]
with self.mock_datetime_and_now("2024-04-22"):
self.attendee_user.sudo()._sync_google_calendar(self.google_service)
for i, event in enumerate(recurrence.calendar_event_ids.sorted("start")):
self.assertEqual(event.attendee_ids[1].state, expected_states[i])
@patch.object(GoogleCalendarService, 'get_events')
def test_accepting_recurrent_event_with_all_events_option_synced_by_organizer(self, mock_get_events):
"""
Test accepting a recurring event with the option "All events" on Google Calendar and syncing the organizer's calendar.
Ensure that all events are accepeted by attendee in Odoo.
"""
recurrence_id = "abcd4"
recurrence = self.generate_recurring_event(
mock_dt="2024-04-20",
google_id=recurrence_id,
rrule="FREQ=DAILY;INTERVAL=1;COUNT=4",
start=datetime(2024, 3, 20, 9, 0),
stop=datetime(2024, 3, 20, 10, 0),
partner_ids=[Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])],
)
google_events = self.google_respond_to_recurrent_event_with_option_all_events(recurrence, "accepted")
mock_get_events.return_value = (
GoogleEvent(google_events), None, [{'method': 'popup', 'minutes': 30}]
)
expected_states = ["accepted", "accepted", "accepted", "accepted"]
with self.mock_datetime_and_now("2024-04-22"):
self.organizer_user.sudo()._sync_google_calendar(self.google_service)
for i, event in enumerate(recurrence.calendar_event_ids.sorted("start")):
self.assertEqual(event.attendee_ids[1].state, expected_states[i])
@patch.object(GoogleCalendarService, 'get_events')
def test_accepting_recurrent_event_with_following_events_option_synced_by_attendee(self, mock_get_events):
"""
Test accepting a recurring event with the option "This and following events" on Google Calendar and syncing the attendee's calendar.
Ensure that affected events are accepeted by attendee in Odoo.
"""
recurrence_id = "abcd5"
recurrence = self.generate_recurring_event(
mock_dt="2024-04-20",
google_id=recurrence_id,
rrule="FREQ=DAILY;INTERVAL=1;COUNT=4",
start=datetime(2024, 3, 20, 9, 0),
stop=datetime(2024, 3, 20, 10, 0),
partner_ids=[Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])],
)
google_events = self.google_respond_to_recurrent_event_with_option_following_events(
recurrence=recurrence,
event_index=2,
response_status="accepted",
rrule1="RRULE:FREQ=DAILY;UNTIL=20240321T215959Z",
rrule2="RRULE:FREQ=DAILY;COUNT=2"
)
mock_get_events.return_value = (
GoogleEvent(google_events), None, [{'method': 'popup', 'minutes': 30}]
)
expected_states = ["needsAction", "needsAction", "accepted", "accepted"]
with self.mock_datetime_and_now("2024-04-22"):
self.attendee_user.sudo()._sync_google_calendar(self.google_service)
for i, event in enumerate(recurrence.calendar_event_ids.sorted("start")):
self.assertEqual(event.attendee_ids[1].state, expected_states[i])
@patch.object(GoogleCalendarService, 'get_events')
def test_accepting_recurrent_event_with_all_following_option_synced_by_organizer(self, mock_get_events):
"""
Test accepting a recurring event with the option "This and following events" on Google Calendar and syncing the organizer's calendar.
Ensure that affected events are accepeted by attendee in Odoo.
"""
recurrence_id = "abcd6"
recurrence = self.generate_recurring_event(
mock_dt="2024-04-20",
google_id=recurrence_id,
rrule="FREQ=DAILY;INTERVAL=1;COUNT=4",
start=datetime(2024, 3, 20, 9, 0),
stop=datetime(2024, 3, 20, 10, 0),
partner_ids=[Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])],
)
google_events = self.google_respond_to_recurrent_event_with_option_following_events(
recurrence=recurrence,
event_index=2,
response_status="accepted",
rrule1="RRULE:FREQ=DAILY;UNTIL=20240321T215959Z",
rrule2="RRULE:FREQ=DAILY;COUNT=2"
)
mock_get_events.return_value = (
GoogleEvent(google_events), None, [{'method': 'popup', 'minutes': 30}]
)
expected_states = ["needsAction", "needsAction", "accepted", "accepted"]
with self.mock_datetime_and_now("2024-04-22"):
self.organizer_user.sudo()._sync_google_calendar(self.google_service)
for i, event in enumerate(recurrence.calendar_event_ids.sorted("start")):
self.assertEqual(event.attendee_ids[1].state, expected_states[i])