[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:
committed by
Yannick Tivisse
parent
ab475628a4
commit
6acdcb6fd8
@@ -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': [],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user