[MERGE] google/microsoft_calendar: Allow a user to stop the synchro

Description of the issue/feature this PR addresses: Give more flexibility to the users by allowing them to stop
the synchronization with Google/Outlook calendar.

Current behavior before PR: Only way to stop the active synchronization with Google/Outlook calendar is
to uninstall both modules.

Desired behavior after PR is merged: Add a "toggle" button that allow to turn on/off the synchronization with Google/
Outlook calendar independently (i.e. possible to turn off Google sync. while leaving Outlook sync. active).

(PS for the review: the source codes for Outlook and Google are almost identical, except for the Calendar Popovers and
the tests for the new stop/restart feature).

Task ID: 2330246

--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr

closes odoo/odoo#60825

Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Odoo's Mergebot
2020-12-10 11:21:54 +01:00
committed by GitHub
25 changed files with 598 additions and 37 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>
@@ -14,6 +14,7 @@
'wizard/reset_account_views.xml',
'views/res_config_settings_views.xml',
'views/res_users_views.xml',
'views/microsoft_calendar_views.xml',
'views/microsoft_calendar_templates.xml',
],
'demo': [],
@@ -13,7 +13,7 @@ class MicrosoftCalendarController(http.Controller):
""" This route/function is called when we want to synchronize Odoo
calendar with Microsoft 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 MicrosoftCalendarController(http.Controller):
}
# If App authorized, and user access accepted, We launch the synchronization
need_refresh = request.env.user.sudo()._sync_microsoft_calendar(MicrosoftCal)
# If synchronization has been stopped
if not need_refresh and request.env.user.microsoft_synchronization_stopped:
return {
"status": "sync_stopped",
"url": ''
}
return {
"status": "need_refresh" if need_refresh else "no_new_event_from_microsoft",
"url": ''
@@ -36,6 +36,12 @@ class Meeting(models.Model):
'user_id', 'privacy',
'attendee_ids', 'alarm_ids', 'location', 'show_as', 'active'}
@api.model
def _restart_microsoft_sync(self):
self.env['calendar.event'].search(self._get_microsoft_sync_domain()).write({
'need_sync_m': True,
})
@api.model_create_multi
def create(self, vals_list):
return super().create([
@@ -70,6 +70,12 @@ class RecurrenceRule(models.Model):
def _get_microsoft_synced_fields(self):
return {'rrule'} | self.env['calendar.event']._get_microsoft_synced_fields()
@api.model
def _restart_microsoft_sync(self):
self.env['calendar.recurrence'].search(self._get_microsoft_sync_domain()).write({
'need_sync_m': True,
})
def _get_microsoft_sync_domain(self):
return [('calendar_event_ids.user_id', '=', self.env.user.id)]
@@ -74,7 +74,7 @@ class MicrosoftSync(models.AbstractModel):
if 'microsoft_id' in vals:
self._from_microsoft_ids.clear_cache(self)
synced_fields = self._get_microsoft_synced_fields()
if 'need_sync_m' not in vals and vals.keys() & synced_fields:
if 'need_sync_m' not in vals and vals.keys() & synced_fields and not self.env.user.microsoft_synchronization_stopped:
fields_to_sync = [x for x in vals.keys() if x in synced_fields]
if fields_to_sync:
vals['need_sync_m'] = True
@@ -95,6 +95,9 @@ class MicrosoftSync(models.AbstractModel):
def create(self, vals_list):
if any(vals.get('microsoft_id') for vals in vals_list):
self._from_microsoft_ids.clear_cache(self)
if self.env.user.microsoft_synchronization_stopped:
for vals in vals_list:
vals.update({'need_sync_m': False})
records = super().create(vals_list)
microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
@@ -361,3 +364,10 @@ class MicrosoftSync(models.AbstractModel):
marks the record to be re-synchronized.
"""
raise NotImplementedError()
@api.model
def _restart_microsoft_sync(self):
""" Turns on the microsoft synchronization for all the events of
a given user.
"""
raise NotImplementedError()
+14 -1
View File
@@ -19,6 +19,7 @@ class User(models.Model):
_inherit = 'res.users'
microsoft_calendar_sync_token = fields.Char('Microsoft Next Sync Token', copy=False)
microsoft_synchronization_stopped = fields.Boolean('Outlook Synchronization stopped', copy=False)
def _microsoft_calendar_authenticated(self):
return bool(self.sudo().microsoft_calendar_rtoken)
@@ -67,6 +68,8 @@ class User(models.Model):
def _sync_microsoft_calendar(self, calendar_service: MicrosoftCalendarService):
self.ensure_one()
if self.microsoft_synchronization_stopped:
return False
full_sync = not bool(self.microsoft_calendar_sync_token)
with microsoft_calendar_token(self) as token:
try:
@@ -94,7 +97,7 @@ class User(models.Model):
@api.model
def _sync_all_microsoft_calendar(self):
""" Cron job """
users = self.env['res.users'].search([('microsoft_calendar_rtoken', '!=', False)])
users = self.env['res.users'].search([('microsoft_calendar_rtoken', '!=', False), ('microsoft_synchronization_stopped', '=', False)])
microsoft = MicrosoftCalendarService(self.env['microsoft.service'])
for user in users:
_logger.info("Calendar Synchro - Starting synchronization for %s", user)
@@ -102,3 +105,13 @@ class User(models.Model):
user.with_user(user).sudo()._sync_microsoft_calendar(microsoft)
except Exception as e:
_logger.exception("[%s] Calendar Synchro - Exception : %s !", user, exception_to_unicode(e))
def stop_microsoft_synchronization(self):
self.ensure_one()
self.sudo().microsoft_synchronization_stopped = True
def restart_microsoft_synchronization(self):
self.ensure_one()
self.sudo().microsoft_synchronization_stopped = False
self.env['calendar.recurrence']._restart_microsoft_sync()
self.env['calendar.event']._restart_microsoft_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;
@@ -60,7 +61,7 @@ const MicrosoftCalendarModel = CalendarModel.include({
fromurl: window.location.href,
}
}, {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.microsoft_is_sync = false;
} else if (result.status === "no_new_event_from_microsoft" || result.status === "need_refresh") {
self.microsoft_is_sync = true;
@@ -68,11 +69,22 @@ const MicrosoftCalendarModel = CalendarModel.include({
return result
});
},
archiveRecords: function (ids, model) {
return this._rpc({
model: model,
method: 'action_archive',
args: [ids],
context: session.user_context,
});
},
});
const MicrosoftCalendarController = CalendarController.include({
custom_events: _.extend({}, CalendarController.prototype.custom_events, {
syncMicrosoftCalendar: '_onSyncMicrosoftCalendar',
stopMicrosoftSynchronization: '_onStopMicrosoftSynchronization',
archiveRecord: '_onArchiveRecord',
}),
@@ -91,7 +103,7 @@ const MicrosoftCalendarController = CalendarController.include({
_onSyncMicrosoftCalendar: function (event) {
var self = this;
return this.model._syncMicrosoftCalendar().then(function (o) {
return this._restartMicrosoftSynchronization().then(() => {return this.model._syncMicrosoftCalendar();}).then(function (o) {
if (o.status === "need_auth") {
Dialog.alert(self, _t("You will be redirected to Outlook to authorize the access to your calendar."), {
confirm_callback: function() {
@@ -114,20 +126,95 @@ const MicrosoftCalendarController = CalendarController.include({
}
} else if (o.status === "need_refresh") {
self.reload();
return event.data.on_refresh();
}
}).then(event.data.on_always, event.data.on_always);
}
},
_onStopMicrosoftSynchronization: function (event) {
var self = this;
Dialog.confirm(this, _t("You are about to stop the synchronization of your calendar with Outlook. Are you sure you want to continue?"), {
confirm_callback: function() {
return self._rpc({
model: 'res.users',
method: 'stop_microsoft_synchronization',
args: [[self.context.uid]],
}).then(() => {
self.displayNotification({
title: _t("Success"),
message: _t("The synchronization with Outlook calendar was successfully stopped."),
type: 'success',
});
}).then(event.data.on_confirm);
},
title: _t('Confirmation'),
});
return event.data.on_always();
},
_restartMicrosoftSynchronization: function () {
return this._rpc({
model: 'res.users',
method: 'restart_microsoft_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 MicrosoftCalendarRenderer = CalendarRenderer.include({
custom_events: _.extend({}, CalendarRenderer.prototype.custom_events, {
archive_event: '_onArchiveEvent',
}),
events: _.extend({}, CalendarRenderer.prototype.events, {
'click .o_microsoft_sync_button': '_onSyncMicrosoftCalendar',
'click .o_stop_microsoft_sync_button': '_onStopMicrosoftSynchronization',
}),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_initMicrosoftPillButton: function() {
this.$microsoftStopButton.css({"cursor":"pointer", "font-size":"0.9em"});
var switchBadgeClass = (elem) => {elem.toggleClass('badge-success'); elem.toggleClass('badge-danger');};
this.$('.o_stop_microsoft_sync_button').hover(() => {
switchBadgeClass(this.$microsoftStopButton);
this.$microsoftStopButton.html("<i class='fa mr-2 fa-times'/>".concat(_t("Stop the Synchronization")));
}, () => {
switchBadgeClass(this.$microsoftStopButton);
this.$microsoftStopButton.html("<i class='fa mr-2 fa-check'/>".concat(_t("Synched with Outlook")));
});
},
_getMicrosoftButton: function () {
return $('<button/>', {
type: 'button',
html: _t("Sync with <b>Outlook</b>"),
class: 'o_microsoft_sync_button w-100 m-auto btn btn-secondary'
});
},
_getMicrosoftStopButton: function () {
return $('<span/>', {
html: _t("Synched with Outlook"),
class: 'w-100 badge badge-pill badge-success border-0 o_stop_microsoft_sync_button'
})
.prepend($('<i/>', {class: "fa mr-2 fa-check"}));
},
/**
* Adds the Sync with Outlook button in the sidebar
*
@@ -137,21 +224,13 @@ const MicrosoftCalendarRenderer = CalendarRenderer.include({
var self = this;
this._super.apply(this, arguments);
this.$microsoftButton = $();
this.$microsoftStopButton = $();
if (this.model === "calendar.event") {
if (this.state.microsoft_is_sync) {
this.$microsoftButton = $('<span/>', {
html: _t("Synched with Outlook"),
class: 'w-100 badge badge-pill badge-success border-0'
})
.prepend($('<i/>', {class: "fa mr-2 fa-check"}))
.appendTo(self.$sidebar);
this.$microsoftStopButton = this._getMicrosoftStopButton().appendTo(self.$sidebar);
this._initMicrosoftPillButton();
} else {
this.$microsoftButton = $('<button/>', {
type: 'button',
html: _t("Sync with <b>Outlook</b>"),
class: 'o_microsoft_sync_button w-100 m-auto btn btn-secondary'
})
.appendTo(self.$sidebar);
this.$microsoftButton = this._getMicrosoftButton().appendTo(self.$sidebar);
}
}
},
@@ -173,8 +252,36 @@ const MicrosoftCalendarRenderer = CalendarRenderer.include({
on_always: function () {
self.$microsoftButton.prop('disabled', false);
},
on_refresh: function () {
if (_.isEmpty(self.$microsoftStopButton)) {
self.$microsoftStopButton = self._getMicrosoftStopButton();
}
self.$microsoftButton.replaceWith(self.$microsoftStopButton);
self._initMicrosoftPillButton();
}
});
},
_onStopMicrosoftSynchronization: function() {
var self = this;
this.$microsoftStopButton.prop('disabled', true);
this.trigger_up('stopMicrosoftSynchronization' , {
on_confirm: function () {
if (_.isEmpty(self.$microsoftButton)) {
self.$microsoftButton = self._getMicrosoftButton();
}
self.$microsoftStopButton.replaceWith(self.$microsoftButton);
},
on_always: function() {
self.$microsoftStopButton.prop('disabled', false);
}
});
},
_onArchiveEvent: function (event) {
this._unselectEvent();
this.trigger_up('archiveRecord', {id: parseInt(event.data.id, 10)});
},
});
return {
@@ -0,0 +1,34 @@
odoo.define('microsoft_calendar.MicrosoftCalendarPopover', function(require) {
"use strict";
const CalendarPopover = require('web.CalendarPopover');
const MicrosoftCalendarPopover = CalendarPopover.include({
events: _.extend({}, CalendarPopover.prototype.events, {
'click .o_cw_popover_archive_m': '_onClickPopoverArchive',
}),
/**
* We only want one 'Archive' button in the popover
* so if Google Sync is active, it takes precedence
* over this popover.
*/
isMEventSyncedAndArchivable() {
if (this.event.extendedProps.record.google_id === undefined) {
return this.event.extendedProps.record.microsoft_id;
}
return !this.event.extendedProps.record.google_id && this.event.extendedProps.record.microsoft_id
},
isEventDeletable() {
return !this.isMEventSyncedAndArchivable() && this._super();
},
_onClickPopoverArchive: function (ev) {
ev.preventDefault();
this.trigger_up('archive_event', {id: this.event.id});
},
});
return MicrosoftCalendarPopover;
});
@@ -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.isMEventSyncedAndArchivable() and widget.isEventDetailsVisible()" href="#" class="btn btn-secondary o_cw_popover_archive_m">Archive</a>
</t>
</t>
</templates>
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_sync_microsoft2odoo
from . import test_sync_odoo2microsoft
@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from unittest.mock import MagicMock, patch
from odoo.tests.common import TransactionCase
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftSync
from odoo.modules.registry import Registry
from odoo.addons.microsoft_account.models.microsoft_service import TIMEOUT
def patch_api(func):
@patch.object(MicrosoftSync, '_microsoft_insert', MagicMock())
@patch.object(MicrosoftSync, '_microsoft_delete', MagicMock())
@patch.object(MicrosoftSync, '_microsoft_patch', MagicMock())
def patched(self, *args, **kwargs):
return func(self, *args, **kwargs)
return patched
@patch.object(User, '_get_microsoft_calendar_token', lambda user: 'dummy-token')
class TestSyncOdoo2Microsoft(TransactionCase):
def setUp(self):
super().setUp()
self.microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
def assertMicrosoftEventInserted(self, values):
MicrosoftSync._microsoft_insert.assert_called_once_with(self.microsoft_service, values)
def assertMicrosoftEventNotInserted(self):
MicrosoftSync._microsoft_insert.assert_not_called()
def assertMicrosoftEventPatched(self, microsoft_id, values, timeout=None):
expected_args = (microsoft_id, values)
expected_kwargs = {'timeout': timeout} if timeout else {}
MicrosoftSync._microsoft_patch.assert_called_once()
args, kwargs = MicrosoftSync._microsoft_patch.call_args
self.assertEqual(args[1:], expected_args) # skip Google service arg
self.assertEqual(kwargs, expected_kwargs)
@patch_api
def test_stop_synchronization(self):
self.env.user.stop_microsoft_synchronization()
self.assertTrue(self.env.user.microsoft_synchronization_stopped, "The microsoft synchronization flag should be switched on")
self.assertFalse(self.env.user._sync_microsoft_calendar(self.microsoft_service), "The microsoft 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.assertMicrosoftEventNotInserted()
@patch_api
def test_restart_synchronization(self):
# Test new event created after stopping synchronization are correctly patched when restarting sync.
self.maxDiff = None
microsoft_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_microsoft_synchronization()
event = self.env['calendar.event'].with_user(user).create({
'microsoft_id': microsoft_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_microsoft_synchronization()
event.with_user(user)._sync_odoo2microsoft(self.microsoft_service)
microsoft_guid = self.env['ir.config_parameter'].sudo().get_param('microsoft_calendar.microsoft_guid', False)
self.assertMicrosoftEventPatched(event.microsoft_id, {
'id': event.microsoft_id,
'start': {'dateTime': '2020-01-15T08:00:00+00:00', 'timeZone': 'Europe/London'},
'end': {'dateTime': '2020-01-15T18:00:00+00:00', 'timeZone': 'Europe/London'},
'subject': 'Event',
'body': {'content': '', 'contentType': 'text'},
'attendees': [],
'isAllDay': False,
'isOrganizer': True,
'isReminderOn': False,
'sensitivity': 'normal',
'showAs': 'busy',
'location': {'displayName': ''},
'organizer': {'emailAddress': {'address': 'jean-luc@opoo.com', 'name': 'Test user Calendar'}},
'reminderMinutesBeforeStart': 0,
'singleValueExtendedProperties': [{
'id': 'String {%s} Name odoo_id' % microsoft_guid,
'value': str(event.id),
}, {
'id': 'String {%s} Name owner_odoo_id' % microsoft_guid,
'value': str(user.id),
}
]
})
@@ -2,6 +2,7 @@
<odoo>
<template id="assets_backend" name="microsoft_calendar assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/microsoft_calendar/static/src/js/microsoft_calendar_popover.js"></script>
<script type="text/javascript" src="/microsoft_calendar/static/src/js/microsoft_calendar.js"></script>
</xpath>
</template>
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<odoo>
<record id="view_microsoft_calendar_event" model="ir.ui.view">
<field name="name">microsoft_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="microsoft_id" invisible="1"/>
</field>
</field>
</record>
</odoo>