[IMP] calendar, *: configure provider

* = google_calendar, microsoft_calendar, web

Purpose:
Allow to install appropriate module and configure calendar provider credentials
in fewer steps, without needing to go to general settings, etc.

This requires that both buttons are visible from the start.

In this commit, we also prepare the ability to extend this configuration wizard
for the appointment onboarding (see ENT PR).

Task-2852375
Part of Task-2900763

Part-of: odoo/odoo#97105
This commit is contained in:
Florian Charlier
2022-09-07 23:10:17 +02:00
parent 61e9d2c861
commit ba2b0020df
16 changed files with 256 additions and 35 deletions
+1
View File
@@ -4,3 +4,4 @@
from . import controllers
from . import models
from . import populate
from . import wizard
+2
View File
@@ -34,6 +34,7 @@ If you need to manage your meetings, you should install the CRM module.
'views/calendar_templates.xml',
'views/calendar_views.xml',
'views/res_partner_views.xml',
'wizard/calendar_provider_config.xml'
],
'installable': True,
'application': True,
@@ -49,6 +50,7 @@ If you need to manage your meetings, you should install the CRM module.
'calendar/static/src/js/calendar_model.js',
'calendar/static/src/js/calendar_view.js',
'calendar/static/src/js/services/calendar_notification_service.js',
'calendar/static/src/views/*',
],
'web.qunit_suite_tests': [
'calendar/static/tests/**/*',
@@ -11,3 +11,5 @@ access_calendar_alarm_manager,access_calendar_alarm_manager,model_calendar_alarm
access_calendar_filters_all,access_calendar_contacts_all,model_calendar_filters,base.group_user,1,1,1,1
access_calendar_filters,access_calendar_contacts,model_calendar_filters,base.group_system,1,1,1,1
access_calendar_recurrence,access_calendar_recurrence,model_calendar_recurrence,base.group_user,1,1,1,1
access_calendar_provider_config_all,calendar.provider.config.all,model_calendar_provider_config,,0,0,0,0
access_calendar_provider_config_manager,calendar.provider.config.manager,model_calendar_provider_config,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
11 access_calendar_filters_all access_calendar_contacts_all model_calendar_filters base.group_user 1 1 1 1
12 access_calendar_filters access_calendar_contacts model_calendar_filters base.group_system 1 1 1 1
13 access_calendar_recurrence access_calendar_recurrence model_calendar_recurrence base.group_user 1 1 1 1
14 access_calendar_provider_config_all calendar.provider.config.all model_calendar_provider_config 0 0 0 0
15 access_calendar_provider_config_manager calendar.provider.config.manager model_calendar_provider_config base.group_system 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -1,5 +1,6 @@
/** @odoo-module alias=calendar.CalendarRenderer **/
import {_t} from 'web.core';
import CalendarRenderer from 'web.CalendarRenderer';
import CalendarPopover from 'web.CalendarPopover';
import session from 'web.session';
@@ -110,6 +111,20 @@ const AttendeeCalendarRenderer = CalendarRenderer.extend({
CalendarPopover: AttendeeCalendarPopover,
eventTemplate: 'Calendar.calendar-box',
}),
events: _.extend({}, CalendarRenderer.prototype.events, {
'click #google_sync_activate': '_onConfigureExternalCalendarGoogle',
'click #microsoft_sync_activate': '_onConfigureExternalCalendarMicrosoft',
}),
_onConfigureExternalCalendarGoogle: function (e) {
e.preventDefault();
this._configureCalendarProviderSync('google');
},
_onConfigureExternalCalendarMicrosoft: function (e) {
e.preventDefault();
this._configureCalendarProviderSync('microsoft');
},
/**
* Add the attendee-id attribute in order to distinct the events when there are
* several attendees in the event.
@@ -131,6 +146,20 @@ const AttendeeCalendarRenderer = CalendarRenderer.extend({
}
return selector;
},
_configureCalendarProviderSync: function (ProviderName) {
this.do_action({
name: _t('Connect your Calendar'),
type: 'ir.actions.act_window',
res_model: 'calendar.provider.config',
views: [[false, "form"]],
view_mode: "form",
target: 'new',
context: {
'default_external_calendar_provider': ProviderName,
'dialog_size': 'medium',
}
});
},
});
export default {
@@ -0,0 +1,71 @@
/** @odoo-module **/
import { FormRenderer } from "@web/views/form/form_renderer";
import { session } from "@web/session";
import { useService } from "@web/core/utils/hooks";
const providerData = {
'google': {
'restart_sync_method': 'restart_google_synchronization',
'sync_route': '/google_calendar/sync_data'
},
'microsoft': {
'restart_sync_method': 'restart_microsoft_synchronization',
'sync_route': '/microsoft_calendar/sync_data'
}
}
export class CalendarProviderConfigFormRenderer extends FormRenderer {
setup() {
super.setup();
this.orm = useService('orm');
this.rpc = useService('rpc');
}
/**
* Activate the external sync for the first time, after installing the
* relevant submodule if necessary.
*
* @private
*/
async onConnect(ev) {
ev.preventDefault();
ev.stopImmediatePropagation();
if (!await this.props.record.save({stayInEdition: true})) {
return; // handled by view
}
await this.orm.call(
this.props.record.resModel,
'action_calendar_prepare_external_provider_sync',
[this.props.record.resId]
)
// See google/microsoft_calendar for the origin of this shortened version
const { restart_sync_method, sync_route } = providerData[this.props.record.data.external_calendar_provider];
await this.orm.call(
'res.users',
restart_sync_method,
[[session.uid]]
);
const response = await this.rpc(
sync_route, {
model: 'calendar.event',
fromurl: window.location.href,
}
);
await this._beforeLeaveContext();
if (response.status === "need_auth") {
window.location.assign(response.url);
} else if (response.status === "need_refresh") {
window.location.reload();
}
}
/**
* Hook to perform additional work before redirecting to external url or reloading.
*
* @private
*/
async _beforeLeaveContext() {
return Promise.resolve();
}
}
@@ -0,0 +1,13 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { formView } from "@web/views/form/form_view";
import { CalendarProviderConfigFormRenderer } from "./calendar_provider_config_form_renderer";
export const CalendarProviderConfigFormView = {
...formView,
Renderer: CalendarProviderConfigFormRenderer,
};
registry.category("views").add("calendar_provider_config_form", CalendarProviderConfigFormView);
@@ -3,7 +3,21 @@
<t t-name="calendar.CalendarView" t-extend="CalendarView">
<t t-jquery="div.o_calendar_sidebar" t-operation="append">
<div id="calendar_sync" class="o_calendar_sync container inline btn-group"/>
<div class="o_calendar_sync text-center" groups="base.group_system">
<span class="text-primary fs-6">Synchronize with:</span>
</div>
<div id="calendar_sync" class="container inline btn-group justify-content-evenly align-items-center" groups="base.group_system">
<div id="google_calendar_sync" class="o_calendar_sync">
<button type="button" id="google_sync_activate" class="btn btn-muted">
<b><i class='fa fa-plug'/> Google</b>
</button>
</div>
<div id="microsoft_calendar_sync" class="o_calendar_sync">
<button type="button" id="microsoft_sync_activate" class="btn btn-muted">
<b><i class='fa fa-plug'/> Outlook</b>
</button>
</div>
</div>
</t>
</t>
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import calendar_provider_config
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.addons.base.models.ir_module import assert_log_admin_access
class CalendarProviderConfig(models.TransientModel):
_name = 'calendar.provider.config'
_description = 'Calendar Provider Configuration Wizard'
external_calendar_provider = fields.Selection([
('google', 'Google'), ('microsoft', 'Outlook')],
"Choose an external calendar to configure", default='google')
# Allow to sync with eventually existing ICP keys without creating them if respective module is not installed
# Using same field names and strings as their respective res.config.settings
cal_client_id = fields.Char(
"Google Client_id",
default=lambda self: self.env['ir.config_parameter'].get_param('google_calendar_client_id'))
cal_client_secret = fields.Char(
"Google Client_key",
default=lambda self: self.env['ir.config_parameter'].get_param('google_calendar_client_secret'))
microsoft_outlook_client_identifier = fields.Char(
"Outlook Client Id",
default=lambda self: self.env['ir.config_parameter'].get_param('microsoft_calendar_client_id'))
microsoft_outlook_client_secret = fields.Char(
"Outlook Client Secret",
default=lambda self: self.env['ir.config_parameter'].get_param('microsoft_calendar_client_secret'))
@assert_log_admin_access
def action_calendar_prepare_external_provider_sync(self):
""" Called by the wizard to configure an external calendar provider without requiring users
to access the general settings page.
Make sure that the provider calendar module is installed or install it. Then, set
the API keys into the applicable config parameters.
"""
self.ensure_one()
calendar_module = self.env['ir.module.module'].search([
('name', '=', f'{self.external_calendar_provider}_calendar')])
if calendar_module.state != 'installed':
calendar_module.button_immediate_install()
if self.external_calendar_provider == 'google':
self.env['ir.config_parameter'].set_param('google_calendar_client_id', self.cal_client_id)
self.env['ir.config_parameter'].set_param('google_calendar_client_secret', self.cal_client_secret)
elif self.external_calendar_provider == 'microsoft':
self.env['ir.config_parameter'].set_param('microsoft_calendar_client_id', self.microsoft_outlook_client_identifier)
self.env['ir.config_parameter'].set_param('microsoft_calendar_client_secret', self.microsoft_outlook_client_secret)
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="calendar_provider_config_view_form" model="ir.ui.view">
<field name="name">calendar.provider.config.view.form</field>
<field name="model">calendar.provider.config</field>
<field name="arch" type="xml">
<form js_class="calendar_provider_config_form">
<field name="external_calendar_provider" widget="radio" options="{'horizontal': true}"/>
<div attrs="{'invisible': [('external_calendar_provider', '!=', 'google')]}">
<img alt="Google Calendar icon" src="/calendar/static/src/img/google_calendar_40.png" style="height: 40px; margin-right: 5px"/>
<span class="me-1 o_form_label">Google Calendar</span>
<a href="https://www.odoo.com/documentation/master/applications/general/calendars/google/google_calendar_credentials.html" title="Read More" class="o_doc_link" target="_blank"></a>
<div class="text-muted mt-2">
Synchronize your calendar with Google Calendar
</div>
<group>
<field name="cal_client_id" attrs="{'required': [('external_calendar_provider', '=', 'google')]}"/>
<field name="cal_client_secret" password="True" attrs="{'required': [('external_calendar_provider', '=', 'google')]}"/>
</group>
</div>
<div attrs="{'invisible': [('external_calendar_provider', '!=', 'microsoft')]}">
<img alt="Microsoft Outlook icon" src="/calendar/static/src/img/microsoft_calendar_40.png" style="height: 40px; margin-right: 5px"/>
<span class="me-1 o_form_label">Outlook Calendar</span>
<a href="https://www.odoo.com/documentation/master/applications/general/calendars/outlook/outlook_calendar.html" title="Read More" class="o_doc_link" target="_blank"></a>
<div class="text-muted mt-2">
Synchronize your calendar with Outlook
</div>
<group>
<field name="microsoft_outlook_client_identifier" attrs="{'required': [('external_calendar_provider', '=', 'microsoft')]}"/>
<field name="microsoft_outlook_client_secret" password="True" attrs="{'required': [('external_calendar_provider', '=', 'microsoft')]}"/>
</group>
</div>
<footer>
<a role="button" title="Connect" class="o_calendar_activate_external_cal btn btn-primary" t-on-click="onConnect">Connect</a>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
@@ -3,7 +3,6 @@ odoo.define('google_calendar.CalendarView', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
const CalendarRenderer = require('@calendar/js/calendar_renderer')[Symbol.for("default")].AttendeeCalendarRenderer;
const CalendarController = require('@calendar/js/calendar_controller')[Symbol.for("default")];
const CalendarModel = require('@calendar/js/calendar_model')[Symbol.for("default")];
@@ -112,12 +111,8 @@ const GoogleCalendarController = CalendarController.include({
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() {
framework.redirect(o.url);
},
title: _t('Redirection'),
});
// allows to configure API keys in modal and will then redirect.
self.renderer._configureCalendarProviderSync('google');
} else if (o.status === "need_config_from_admin") {
if (!_.isUndefined(o.action) && parseInt(o.action)) {
Dialog.confirm(self, _t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"), {
@@ -1,16 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-inherit="calendar.CalendarView" t-inherit-mode="extension">
<xpath expr="//div[@id='calendar_sync']" position="inside">
<button type="button" id="google_sync_pending" class="o_google_sync_button o_google_sync_pending btn btn-secondary btn">
<b><i class='fa fa-refresh'/> Google</b>
</button>
<button type="button" id="google_sync_configured" class="me-1 o_google_sync_button o_google_sync_button_configured btn text-bg-primary">
<b>
<i id="google_check" class='fa fa-check'/>
<i id="google_stop" class='fa fa-times' style="display: none"/>&amp;nbsp;Google
</b>
</button>
<xpath expr="//div[@id='google_calendar_sync']" position="replace">
<div id="google_calendar_sync" class="o_calendar_sync">
<button type="button" id="google_sync_pending" class="o_google_sync_button o_google_sync_pending btn btn-secondary btn">
<b><i class='fa fa-refresh'/> Google</b>
</button>
<button type="button" id="google_sync_configured" class="me-1 o_google_sync_button o_google_sync_button_configured btn text-bg-primary">
<b>
<i id="google_check" class='fa fa-check'/>
<i id="google_stop" class='fa fa-times' style="display: none"/>&amp;nbsp;Google
</b>
</button>
</div>
</xpath>
</t>
</templates>
@@ -3,7 +3,6 @@ odoo.define('microsoft_calendar.CalendarView', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
const CalendarRenderer = require('@calendar/js/calendar_renderer')[Symbol.for("default")].AttendeeCalendarRenderer;
const CalendarController = require('@calendar/js/calendar_controller')[Symbol.for("default")];
const CalendarModel = require('@calendar/js/calendar_model')[Symbol.for("default")];
@@ -111,12 +110,8 @@ const MicrosoftCalendarController = CalendarController.include({
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() {
framework.redirect(o.url);
},
title: _t('Redirection'),
});
// allows to configure API keys in modal and will then redirect.
self.renderer._configureCalendarProviderSync('microsoft');
} else if (o.status === "need_config_from_admin") {
if (!_.isUndefined(o.action) && parseInt(o.action)) {
Dialog.confirm(self, _t("The Outlook Synchronization needs to be configured before you can use it, do you want to do it now?"), {
@@ -1,16 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-inherit="calendar.CalendarView" t-inherit-mode="extension">
<xpath expr="//div[@id='calendar_sync']" position="inside">
<button type="button" id="microsoft_sync_pending" class="o_microsoft_sync_button o_microsoft_sync_pending btn btn-secondary btn">
<b><i class='fa fa-refresh'/> Outlook</b>
</button>
<button type="button" id="microsoft_sync_configured" class="me-1 o_microsoft_sync_button o_microsoft_sync_button_configured btn text-bg-primary">
<b>
<i id="microsoft_check" class='fa fa-check'/>
<i id="microsoft_stop" class='fa fa-times' style="display: none"/>&amp;nbsp;Outlook
</b>
</button>
<xpath expr="//div[@id='microsoft_calendar_sync']" position="replace">
<div id="microsoft_calendar_sync" class="o_calendar_sync">
<button type="button" id="microsoft_sync_pending" class="o_microsoft_sync_button o_microsoft_sync_pending btn btn-secondary btn">
<b><i class='fa fa-refresh'/> Outlook</b>
</button>
<button type="button" id="microsoft_sync_configured" class="me-1 o_microsoft_sync_button o_microsoft_sync_button_configured btn text-bg-primary">
<b>
<i id="microsoft_check" class='fa fa-check'/>
<i id="microsoft_stop" class='fa fa-times' style="display: none"/>&amp;nbsp;Outlook
</b>
</button>
</div>
</xpath>
</t>
</templates>