[IMP] google_calendar: add stop synchronization button

Add the possibility to stop/restart the synchronization with
Google Calendar.

Change the calendar event popover of the calendar view to
display an 'Archive' button if the event was synced at least
one.

Add some tests for the new stop/restart features.
This commit is contained in:
Aurélien (avd)
2020-12-10 10:23:10 +01:00
committed by Yannick Tivisse
parent ab475628a4
commit 6acdcb6fd8
12 changed files with 267 additions and 19 deletions
+1
View File
@@ -14,6 +14,7 @@
'wizard/reset_account_views.xml',
'views/res_config_settings_views.xml',
'views/res_users_views.xml',
'views/google_calendar_views.xml',
'views/google_calendar_templates.xml',
],
'demo': [],
+8 -1
View File
@@ -13,7 +13,7 @@ class GoogleCalendarController(http.Controller):
""" This route/function is called when we want to synchronize Odoo
calendar with Google Calendar.
Function return a dictionary with the status : need_config_from_admin, need_auth,
need_refresh, success if not calendar_event
need_refresh, sync_stopped, success if not calendar_event
The dictionary may contains an url, to allow Odoo Client to redirect user on
this URL for authorization for example
"""
@@ -42,6 +42,13 @@ class GoogleCalendarController(http.Controller):
}
# If App authorized, and user access accepted, We launch the synchronization
need_refresh = request.env.user.sudo()._sync_google_calendar(GoogleCal)
# If synchronization has been stopped
if not need_refresh and request.env.user.google_synchronization_stopped:
return {
"status": "sync_stopped",
"url": ''
}
return {
"status": "need_refresh" if need_refresh else "no_new_event_from_google",
"url": ''
@@ -33,6 +33,12 @@ class Meeting(models.Model):
return {'name', 'description', 'allday', 'start', 'date_end', 'stop',
'attendee_ids', 'alarm_ids', 'location', 'privacy', 'active'}
@api.model
def _restart_google_sync(self):
self.env['calendar.event'].search(self._get_sync_domain()).write({
'need_sync': True,
})
@api.model_create_multi
def create(self, vals_list):
return super().create([
@@ -79,6 +79,12 @@ class RecurrenceRule(models.Model):
def _get_google_synced_fields(self):
return {'rrule'}
@api.model
def _restart_google_sync(self):
self.env['calendar.recurrence'].search(self._get_sync_domain()).write({
'need_sync': True,
})
@api.model
def _sync_google2odoo(self, *args, **kwargs):
synced_recurrences = super()._sync_google2odoo(*args, **kwargs)
+11 -1
View File
@@ -72,7 +72,7 @@ class GoogleSync(models.AbstractModel):
if 'google_id' in vals:
self._from_google_ids.clear_cache(self)
synced_fields = self._get_google_synced_fields()
if 'need_sync' not in vals and vals.keys() & synced_fields:
if 'need_sync' not in vals and vals.keys() & synced_fields and not self.env.user.google_synchronization_stopped:
vals['need_sync'] = True
result = super().write(vals)
@@ -86,6 +86,9 @@ class GoogleSync(models.AbstractModel):
def create(self, vals_list):
if any(vals.get('google_id') for vals in vals_list):
self._from_google_ids.clear_cache(self)
if self.env.user.google_synchronization_stopped:
for vals in vals_list:
vals.update({'need_sync': False})
records = super().create(vals_list)
google_service = GoogleCalendarService(self.env['google.service'])
@@ -244,3 +247,10 @@ class GoogleSync(models.AbstractModel):
marks the record to be re-synchronized.
"""
raise NotImplementedError()
@api.model
def _restart_google_sync(self):
""" Turns on the google synchronization for all the events of
a given user.
"""
raise NotImplementedError()
+15 -2
View File
@@ -23,7 +23,8 @@ class User(models.Model):
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)
def _set_auth_tokens(self, access_token, refresh_token, ttl):
self.write({
'google_calendar_rtoken': refresh_token,
@@ -79,6 +80,8 @@ class User(models.Model):
def _sync_google_calendar(self, calendar_service: GoogleCalendarService):
self.ensure_one()
if self.google_synchronization_stopped:
return False
full_sync = not bool(self.google_calendar_sync_token)
with google_calendar_token(self) as token:
try:
@@ -106,7 +109,7 @@ class User(models.Model):
@api.model
def _sync_all_google_calendar(self):
""" Cron job """
users = self.env['res.users'].search([('google_calendar_rtoken', '!=', False)])
users = self.env['res.users'].search([('google_calendar_rtoken', '!=', False), ('google_synchronization_stopped', '=', False)])
google = GoogleCalendarService(self.env['google.service'])
for user in users:
_logger.info("Calendar Synchro - Starting synchronization for %s", user)
@@ -114,3 +117,13 @@ class User(models.Model):
user.with_user(user).sudo()._sync_google_calendar(google)
except Exception as e:
_logger.exception("[%s] Calendar Synchro - Exception : %s !", user, exception_to_unicode(e))
def stop_google_synchronization(self):
self.ensure_one()
self.sudo().google_synchronization_stopped = True
def restart_google_synchronization(self):
self.ensure_one()
self.sudo().google_synchronization_stopped = False
self.env['calendar.recurrence']._restart_google_sync()
self.env['calendar.event']._restart_google_sync()
@@ -9,6 +9,7 @@ const CalendarRenderer = require('calendar.CalendarRenderer');
const CalendarController = require('calendar.CalendarController');
const CalendarModel = require('calendar.CalendarModel');
const viewRegistry = require('web.view_registry');
const session = require('web.session');
var _t = core._t;
@@ -63,7 +64,7 @@ const GoogleCalendarModel = CalendarModel.include({
local_context: context, // LUL TODO remove this local_context
}
}, {shadow}).then(function (result) {
if (result.status === "need_config_from_admin" || result.status === "need_auth") {
if (["need_config_from_admin", "need_auth", "sync_stopped"].includes(result.status)) {
self.google_is_sync = false;
} else if (result.status === "no_new_event_from_google" || result.status === "need_refresh") {
self.google_is_sync = true;
@@ -71,11 +72,22 @@ const GoogleCalendarModel = CalendarModel.include({
return result
});
},
archiveRecords: function (ids, model) {
return this._rpc({
model: model,
method: 'action_archive',
args: [ids],
context: session.user_context,
});
},
})
const GoogleCalendarController = CalendarController.include({
custom_events: _.extend({}, CalendarController.prototype.custom_events, {
syncGoogleCalendar: '_onGoogleSyncCalendar',
stopGoogleSynchronization: '_onStopGoogleSynchronization',
archiveRecord: '_onArchiveRecord',
}),
@@ -94,7 +106,7 @@ const GoogleCalendarController = CalendarController.include({
_onGoogleSyncCalendar: function (event) {
var self = this;
return this.model._syncGoogleCalendar().then(function (o) {
return this._restartGoogleSynchronization().then(() => {return this.model._syncGoogleCalendar();}).then(function (o) {
if (o.status === "need_auth") {
Dialog.alert(self, _t("You will be redirected to Google to authorize access to your calendar!"), {
confirm_callback: function() {
@@ -117,20 +129,95 @@ const GoogleCalendarController = CalendarController.include({
}
} else if (o.status === "need_refresh") {
self.reload();
return event.data.on_refresh();
}
}).then(event.data.on_always, event.data.on_always);
}
},
_onStopGoogleSynchronization: function (event) {
var self = this;
Dialog.confirm(this, _t("You are about to stop the synchronization of your calendar with Google. Are you sure you want to continue?"), {
confirm_callback: function() {
return self._rpc({
model: 'res.users',
method: 'stop_google_synchronization',
args: [[self.context.uid]],
}).then(() => {
self.displayNotification({
title: _t("Success"),
message: _t("The synchronization with Google calendar was successfully stopped."),
type: 'success',
});
}).then(event.data.on_confirm);
},
title: _t('Confirmation'),
});
return event.data.on_always();
},
_restartGoogleSynchronization: function () {
return this._rpc({
model: 'res.users',
method: 'restart_google_synchronization',
args: [[this.context.uid]],
});
},
_onArchiveRecord: function (event) {
var self = this;
Dialog.confirm(this, _t("Are you sure you want to archive this record ?"), {
confirm_callback: function () {
self.model.archiveRecords([event.data.id], self.modelName).then(function () {
self.reload();
});
}
});
},
});
const GoogleCalendarRenderer = CalendarRenderer.include({
custom_events: _.extend({}, CalendarRenderer.prototype.custom_events, {
archive_event: '_onArchiveEvent',
}),
events: _.extend({}, CalendarRenderer.prototype.events, {
'click .o_google_sync_button': '_onGoogleSyncCalendar',
'click .o_stop_google_sync_button': '_onStopGoogleSynchronization',
}),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_initGooglePillButton: function() {
this.$googleStopButton.css({"cursor":"pointer", "font-size":"0.9em"});
var switchBadgeClass = (elem) => {elem.toggleClass('badge-success'); elem.toggleClass('badge-danger');};
this.$('.o_stop_google_sync_button').hover(() => {
switchBadgeClass(this.$googleStopButton);
this.$googleStopButton.html("<i class='fa mr-2 fa-times'/>".concat(_t("Stop the Synchronization")));
}, () => {
switchBadgeClass(this.$googleStopButton);
this.$googleStopButton.html("<i class='fa mr-2 fa-check'/>".concat(_t("Synched with Google")));
});
},
_getGoogleButton: function () {
return $('<button/>', {
type: 'button',
html: _t("Sync with <b>Google</b>"),
class: 'o_google_sync_button w-100 m-auto btn btn-secondary'
});
},
_getGoogleStopButton: function () {
return $('<span/>', {
html: _t("Synched with Google"),
class: 'w-100 badge badge-pill badge-success border-0 o_stop_google_sync_button'
})
.prepend($('<i/>', {class: "fa mr-2 fa-check"}));
},
/**
* Adds the Sync with Google button in the sidebar
*
@@ -140,21 +227,13 @@ const GoogleCalendarRenderer = CalendarRenderer.include({
var self = this;
this._super.apply(this, arguments);
this.$googleButton = $();
this.$googleStopButton = $();
if (this.model === "calendar.event") {
if (this.state.google_is_sync) {
this.$googleButton = $('<span/>', {
html: _t("Synched with Google"),
class: 'w-100 badge badge-pill badge-success border-0'
})
.prepend($('<i/>', {class: "fa mr-2 fa-check"}))
.appendTo(self.$sidebar);
this.$googleStopButton = this._getGoogleStopButton().appendTo(self.$sidebar);
this._initGooglePillButton();
} else {
this.$googleButton = $('<button/>', {
type: 'button',
html: _t("Sync with <b>Google</b>"),
class: 'o_google_sync_button w-100 m-auto btn btn-secondary'
})
.appendTo(self.$sidebar);
this.$googleButton = this._getGoogleButton().appendTo(self.$sidebar);
}
}
},
@@ -176,8 +255,36 @@ const GoogleCalendarRenderer = CalendarRenderer.include({
on_always: function () {
self.$googleButton.prop('disabled', false);
},
on_refresh: function () {
if (_.isEmpty(self.$googleStopButton)) {
self.$googleStopButton = self._getGoogleStopButton();
}
self.$googleButton.replaceWith(self.$googleStopButton);
self._initGooglePillButton();
}
});
},
_onStopGoogleSynchronization: function() {
var self = this;
this.$googleStopButton.prop('disabled', true);
this.trigger_up('stopGoogleSynchronization' , {
on_confirm: function () {
if (_.isEmpty(self.$googleButton)) {
self.$googleButton = self._getGoogleButton();
}
self.$googleStopButton.replaceWith(self.$googleButton);
},
on_always: function() {
self.$googleStopButton.prop('disabled', false);
}
});
},
_onArchiveEvent: function (event) {
this._unselectEvent();
this.trigger_up('archiveRecord', {id: parseInt(event.data.id, 10)});
},
});
return {
@@ -0,0 +1,26 @@
odoo.define('google_calendar.GoogleCalendarPopover', function(require) {
"use strict";
const CalendarPopover = require('web.CalendarPopover');
const GoogleCalendarPopover = CalendarPopover.include({
events: _.extend({}, CalendarPopover.prototype.events, {
'click .o_cw_popover_archive_g': '_onClickPopoverGArchive',
}),
isGEventSyncedAndArchivable() {
return this.event.extendedProps.record.google_id;
},
isEventDeletable() {
return !this.isGEventSyncedAndArchivable() && this._super();
},
_onClickPopoverGArchive: function (ev) {
ev.preventDefault();
this.trigger_up('archive_event', {id: this.event.id});
},
});
return GoogleCalendarPopover;
});
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-extend="Calendar.attendee.status.popover">
<t t-jquery=".o_cw_popover_edit" t-operation="after">
<a t-if="widget.isGEventSyncedAndArchivable() and widget.isEventDetailsVisible()" href="#" class="btn btn-secondary o_cw_popover_archive_g">Archive</a>
</t>
</t>
</templates>
@@ -234,3 +234,53 @@ class TestSyncOdoo2Google(TransactionCase):
'reminders': {'overrides': [], 'useDefault': False},
'visibility': 'public',
}, timeout=3)
@patch_api
def test_stop_synchronization(self):
self.env.user.stop_google_synchronization()
self.assertTrue(self.env.user.google_synchronization_stopped, "The google synchronization flag should be switched on")
self.assertFalse(self.env.user._sync_google_calendar(self.google_service), "The google synchronization should be stopped")
# If synchronization stopped, creating a new event should not call _google_insert.
self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'privacy': 'private',
})
self.assertGoogleEventNotInserted()
@patch_api
def test_restart_synchronization(self):
# Test new event created after stopping synchronization are correctly patched when restarting sync.
google_id = 'aaaaaaaaa'
partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
user = self.env['res.users'].create({
'name': 'Test user Calendar',
'login': 'jean-luc@opoo.com',
'partner_id': partner.id,
})
user.stop_google_synchronization()
event = self.env['calendar.event'].with_user(user).create({
'google_id': google_id,
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'partner_ids': [(4, partner.id)],
})
user.with_user(user).restart_google_synchronization()
self.assertGoogleEventPatched(event.google_id, {
'id': event.google_id,
'start': {'dateTime': '2020-01-15T08:00:00+00:00'},
'end': {'dateTime': '2020-01-15T18:00:00+00:00'},
'summary': 'Event',
'description': '',
'location': '',
'visibility': 'public',
'guestsCanModify': True,
'reminders': {'overrides': [], 'useDefault': False},
'organizer': {'email': 'jean-luc@opoo.com', 'self': True},
'attendees': [{'email': 'jean-luc@opoo.com', 'responseStatus': 'accepted'}],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}}
}, timeout=3)
@@ -2,6 +2,7 @@
<odoo>
<template id="assets_backend" name="google_calendar assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/google_calendar/static/src/js/google_calendar_popover.js"></script>
<script type="text/javascript" src="/google_calendar/static/src/js/google_calendar.js"></script>
</xpath>
</template>
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<odoo>
<record id="view_google_calendar_event" model="ir.ui.view">
<field name="name">google_calendar.event.calendar</field>
<field name="model">calendar.event</field>
<field name="inherit_id" ref="calendar.view_calendar_event_calendar"/>
<field name="arch" type="xml">
<field name="attendee_status" position="after">
<field name="google_id" invisible="1"/>
</field>
</field>
</record>
</odoo>