Initial commit: Security packages
|
After Width: | Height: | Size: 6 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,116 @@
|
|||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<h2 class="oe_slogan">Google Calendar</h2>
|
||||
<h3 class="oe_slogan">Get your meetings, your leaves... Get your calendar anywhere and never forget an event.</h3>
|
||||
<div class="oe_span12">
|
||||
<img src="the_calendar.png" class="oe_picture oe_screenshot">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container oe_dark">
|
||||
<div class="oe_row">
|
||||
<h2 class="oe_slogan">Keep an eye on your events</h2>
|
||||
<div class="oe_span6">
|
||||
<p class='oe_mt32'>
|
||||
See easily the purpose of the meeting, the start time and also the attendee(s)... All that without click on anything...
|
||||
</p>
|
||||
</div>
|
||||
<div class="oe_span6">
|
||||
<img class="oe_picture oe_screenshot" src="an_event.png">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container">
|
||||
<div class="oe_row">
|
||||
<h2 class="oe_slogan">Create so easily an event</h2>
|
||||
<div class="oe_span6">
|
||||
<img class="oe_picture oe_screenshot" src="create_quick.png">
|
||||
</div>
|
||||
<div class="oe_span6">
|
||||
<p class='oe_mt32'>
|
||||
In just one click you can create an event...<br/>
|
||||
You can drag and drop your event if you want moved it to another timing.<br/>
|
||||
You can shrink or extend the event if you need to change the start's hours or the duration of your meeting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container oe_dark">
|
||||
<div class="oe_row">
|
||||
<h2 class="oe_slogan">Create recurrent event</h2>
|
||||
<div class="oe_span6">
|
||||
<p class='oe_mt32'>
|
||||
You can also create recurrent events with only one event.<br/>
|
||||
You need to create an event each monday of the week ? With only one it's possible, you could specify the recurrence and if one of this event is moved, or deleted, it's not a problem, you can untie your event from the others recurrences.
|
||||
</p>
|
||||
</div>
|
||||
<div class="oe_span6">
|
||||
<img class="oe_picture oe_screenshot" src="recurrent.png">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container ">
|
||||
<div class="oe_row">
|
||||
<h2 class="oe_slogan">See all events you wants </h2>
|
||||
<div class="oe_span6">
|
||||
<img class="oe_picture oe_screenshot" src="coworker.png">
|
||||
</div>
|
||||
<div class="oe_span6">
|
||||
<p class='oe_mt32'>
|
||||
See in your calendar, the event from others peoples where your are attendee, but also their events by simply adding your favorites coworkers.<br/>
|
||||
Every coworker will have their own color in your calendar, and every attendee will have their avatar in the event...<br/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container oe_dark">
|
||||
<div class="oe_row">
|
||||
<h2 class="oe_slogan">Get an email</h2>
|
||||
<div class="oe_span6">
|
||||
<p class='oe_mt32'>
|
||||
You will receive an email at creation of an event where you are attendee, but also when this event is updated for some fields as date start, ...
|
||||
</p>
|
||||
</div>
|
||||
<div class="oe_span6">
|
||||
<img class="oe_picture oe_screenshot" src="email.png">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container ">
|
||||
<div class="oe_row">
|
||||
<h2 class="oe_slogan">Be notified </h2>
|
||||
<div class="oe_span6">
|
||||
<img class="oe_picture oe_screenshot" src="notification.png">
|
||||
</div>
|
||||
<div class="oe_span6">
|
||||
<p class='oe_mt32'>
|
||||
You can ask to have a alarm of type 'notification' in your Odoo.<br/>
|
||||
You will have a notification in you Odoo which ever the page you are.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<section class="oe_container oe_dark">
|
||||
<div class="oe_row">
|
||||
<h2 class="oe_slogan">Google Calendar</h2>
|
||||
<div class="oe_span6">
|
||||
<p class='oe_mt32'>
|
||||
With the plugin Google_calendar, you can synchronize your Odoo calendar with Google Calendar.
|
||||
</p>
|
||||
</div>
|
||||
<div class="oe_span6">
|
||||
<img class="oe_picture oe_screenshot" src="calendar_in_action.png">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,25 @@
|
|||
.o_calendar_sidebar {
|
||||
.bg-primary {
|
||||
background-color: $o-enterprise-primary-color;
|
||||
}
|
||||
.o_google_sync_button {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.o_google_sync_button_configured {
|
||||
color: white;
|
||||
height: 2em;
|
||||
|
||||
&:hover {
|
||||
#google_check {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:not(:hover) {
|
||||
#google_stop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { AttendeeCalendarCommonPopover } from "@calendar/views/attendee_calendar/common/attendee_calendar_common_popover";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(AttendeeCalendarCommonPopover.prototype, "google_calendar_google_calendar_common_popover", {
|
||||
get isEventArchivable() {
|
||||
return this._super() || (this.isCurrentUserOrganizer && this.props.record.rawRecord.google_id);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { AttendeeCalendarController } from "@calendar/views/attendee_calendar/attendee_calendar_controller";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ConfirmationDialog, AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
patch(AttendeeCalendarController.prototype, "google_calendar_google_calendar_controller", {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
this.dialog = useService("dialog");
|
||||
this.notification = useService("notification");
|
||||
},
|
||||
|
||||
async onGoogleSyncCalendar() {
|
||||
await this.orm.call(
|
||||
"res.users",
|
||||
"restart_google_synchronization",
|
||||
[[this.user.userId]],
|
||||
);
|
||||
const syncResult = await this.model.syncGoogleCalendar();
|
||||
if (syncResult.status === "need_auth") {
|
||||
window.location.assign(syncResult.url);
|
||||
} else if (syncResult.status === "need_config_from_admin") {
|
||||
if (this.isSystemUser) {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
title: this.env._t("Configuration"),
|
||||
body: this.env._t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"),
|
||||
confirm: this.actionService.doAction.bind(this.actionService, syncResult.action),
|
||||
});
|
||||
} else {
|
||||
this.dialog.add(AlertDialog, {
|
||||
title: this.env._t("Configuration"),
|
||||
body: this.env._t("An administrator needs to configure Google Synchronization before you can use it!"),
|
||||
});
|
||||
}
|
||||
} else if (syncResult.status === "need_refresh") {
|
||||
await this.model.load();
|
||||
}
|
||||
},
|
||||
|
||||
async onStopGoogleSynchronization() {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: this.env._t("You are about to stop the synchronization of your calendar with Google. Are you sure you want to continue?"),
|
||||
confirm: async () => {
|
||||
await this.orm.call(
|
||||
"res.users",
|
||||
"stop_google_synchronization",
|
||||
[[this.user.userId]],
|
||||
);
|
||||
this.notification.add(
|
||||
this.env._t("The synchronization with Google calendar was successfully stopped."),
|
||||
{
|
||||
title: this.env._t("Success"),
|
||||
type: "success",
|
||||
},
|
||||
);
|
||||
await this.model.load();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="google_calendar.GoogleCalendarController" t-inherit="calendar.AttendeeCalendarController" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[@id='calendar_sync_wrapper']" position="attributes">
|
||||
<attribute name="t-if">true</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='google_calendar_sync']" position="replace">
|
||||
<div id="google_calendar_sync" class="o_calendar_sync">
|
||||
<button t-if="!model.googleIsSync" type="button" id="google_sync_pending" class="o_google_sync_button o_google_sync_pending btn btn-secondary btn" t-on-click="onGoogleSyncCalendar">
|
||||
<b><i class='fa fa-refresh'/> Google</b>
|
||||
</button>
|
||||
<!-- class change on hover -->
|
||||
<button t-else="" type="button" id="google_sync_configured" class="me-1 o_google_sync_button o_google_sync_button_configured btn text-bg-primary" t-on-click="onStopGoogleSynchronization"
|
||||
t-on-mouseenter="(ev) => {ev.target.classList.remove('text-bg-primary');ev.target.classList.add('text-bg-danger');}"
|
||||
t-on-mouseleave="(ev) => {ev.target.classList.remove('text-bg-danger');ev.target.classList.add('text-bg-primary');}">
|
||||
<b>
|
||||
<i id="google_check" class='fa fa-check'/>
|
||||
<i id="google_stop" class='fa fa-times'/>
|
||||
<span class="mx-1">Google</span>
|
||||
</b>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { AttendeeCalendarModel } from "@calendar/views/attendee_calendar/attendee_calendar_model";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(AttendeeCalendarModel, "google_calendar_google_calendar_model", {
|
||||
services: [...AttendeeCalendarModel.services, "rpc"],
|
||||
});
|
||||
|
||||
patch(AttendeeCalendarModel.prototype, "google_calendar_google_calendar_model_functions", {
|
||||
setup(params, { rpc }) {
|
||||
this._super(...arguments);
|
||||
this.rpc = rpc;
|
||||
this.isAlive = params.isAlive;
|
||||
this.googleIsSync = true;
|
||||
this.googlePendingSync = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async updateData() {
|
||||
const _super = this._super.bind(this);
|
||||
if (this.googlePendingSync) {
|
||||
return _super(...arguments);
|
||||
}
|
||||
try {
|
||||
await Promise.race([
|
||||
new Promise(resolve => setTimeout(resolve, 1000)),
|
||||
this.syncGoogleCalendar(true)
|
||||
]);
|
||||
} catch (error) {
|
||||
if (error.event) {
|
||||
error.event.preventDefault();
|
||||
}
|
||||
console.error("Could not synchronize Google events now.", error);
|
||||
this.googlePendingSync = false;
|
||||
}
|
||||
if (this.isAlive()) {
|
||||
return _super(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
async syncGoogleCalendar(silent = false) {
|
||||
this.googlePendingSync = true;
|
||||
const result = await this.rpc(
|
||||
"/google_calendar/sync_data",
|
||||
{
|
||||
model: this.resModel,
|
||||
fromurl: window.location.href
|
||||
},
|
||||
{
|
||||
silent,
|
||||
},
|
||||
);
|
||||
if (["need_config_from_admin", "need_auth", "sync_stopped"].includes(result.status)) {
|
||||
this.googleIsSync = false;
|
||||
} else if (result.status === "no_new_event_from_google" || result.status === "need_refresh") {
|
||||
this.googleIsSync = true;
|
||||
}
|
||||
this.googlePendingSync = false;
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, "google_calendar_mock_server", {
|
||||
/**
|
||||
* Simulate the creation of a custom appointment type
|
||||
* by receiving a list of slots.
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/google_calendar/sync_data') {
|
||||
return Promise.resolve({status: 'no_new_event_from_google'});
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture, patchDate, makeDeferred, nextTick} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
const uid = -1;
|
||||
|
||||
QUnit.module('Google Calendar', {
|
||||
beforeEach: function () {
|
||||
patchDate(2016, 11, 12, 8, 0, 0);
|
||||
serverData = {
|
||||
models: {
|
||||
'calendar.event': {
|
||||
fields: {
|
||||
id: {string: "ID", type: "integer"},
|
||||
user_id: {string: "user", type: "many2one", relation: 'user'},
|
||||
partner_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.partner_id'},
|
||||
name: {string: "name", type: "char"},
|
||||
start_date: {string: "start date", type: "date"},
|
||||
stop_date: {string: "stop date", type: "date"},
|
||||
start: {string: "start datetime", type: "datetime"},
|
||||
stop: {string: "stop datetime", type: "datetime"},
|
||||
allday: {string: "allday", type: "boolean"},
|
||||
partner_ids: {string: "attendees", type: "one2many", relation: 'partner'},
|
||||
type: {string: "type", type: "integer"},
|
||||
},
|
||||
records: [
|
||||
{id: 5, user_id: uid, partner_id: 4, name: "event 1", start: "2016-12-13 15:55:05", stop: "2016-12-15 18:55:05", allday: false, partner_ids: [4], type: 2},
|
||||
{id: 6, user_id: uid, partner_id: 5, name: "event 2", start: "2016-12-18 08:00:00", stop: "2016-12-18 09:00:00", allday: false, partner_ids: [4], type: 3}
|
||||
],
|
||||
check_access_rights: function () {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
},
|
||||
'appointment.type': {
|
||||
fields: {},
|
||||
records: [],
|
||||
},
|
||||
user: {
|
||||
fields: {
|
||||
id: {string: "ID", type: "integer"},
|
||||
display_name: {string: "Displayed name", type: "char"},
|
||||
partner_id: {string: "partner", type: "many2one", relation: 'partner'},
|
||||
image_1920: {string: "image", type: "integer"},
|
||||
},
|
||||
records: [
|
||||
{id: 4, display_name: "user 4", partner_id: 4},
|
||||
]
|
||||
},
|
||||
partner: {
|
||||
fields: {
|
||||
id: {string: "ID", type: "integer"},
|
||||
display_name: {string: "Displayed name", type: "char"},
|
||||
image_1920: {string: "image", type: "integer"},
|
||||
},
|
||||
records: [
|
||||
{id: 4, display_name: "partner 4", image_1920: 'DDD'},
|
||||
{id: 5, display_name: "partner 5", image_1920: 'DDD'},
|
||||
]
|
||||
},
|
||||
filter_partner: {
|
||||
fields: {
|
||||
id: {string: "ID", type: "integer"},
|
||||
user_id: {string: "user", type: "many2one", relation: 'user'},
|
||||
partner_id: {string: "partner", type: "many2one", relation: 'partner'},
|
||||
partner_checked: {string: "checked", type: "boolean"},
|
||||
},
|
||||
records: [
|
||||
{id: 3, user_id: uid, partner_id: 4, partner_checked: true}
|
||||
]
|
||||
},
|
||||
},
|
||||
views: {},
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
serviceRegistry.add(
|
||||
"user",
|
||||
{
|
||||
...userService,
|
||||
start() {
|
||||
const fakeUserService = userService.start(...arguments);
|
||||
Object.defineProperty(fakeUserService, "userId", {
|
||||
get: () => uid,
|
||||
});
|
||||
return fakeUserService;
|
||||
},
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.test('sync google calendar', async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
let id = 7;
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: 'calendar.event',
|
||||
serverData,
|
||||
arch:
|
||||
'<calendar class="o_calendar_test" '+
|
||||
'js_class="attendee_calendar" '+
|
||||
'date_start="start" '+
|
||||
'date_stop="stop" '+
|
||||
'attendee="partner_ids" '+
|
||||
'mode="month">'+
|
||||
'<field name="name"/>'+
|
||||
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
|
||||
'</calendar>',
|
||||
mockRPC: async function (route, args) {
|
||||
if (route === '/google_calendar/sync_data') {
|
||||
assert.step(route);
|
||||
serverData.models['calendar.event'].records.push(
|
||||
{id: id++, user_id: uid, partner_id: 4, name: "event from google calendar", start: "2016-12-28 15:55:05", stop: "2016-12-29 18:55:05", allday: false, partner_ids: [4], type: 4}
|
||||
);
|
||||
return Promise.resolve({status: 'need_refresh'});
|
||||
} else if (route === '/web/dataset/call_kw/calendar.event/search_read') {
|
||||
assert.step(route);
|
||||
} else if (route === '/web/dataset/call_kw/res.partner/get_attendee_detail') {
|
||||
return Promise.resolve([]);
|
||||
} else if (route === '/web/dataset/call_kw/res.users/has_group') {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
// select the partner filter
|
||||
await click(target.querySelector('.o_calendar_filter_item[data-value=all] input'));
|
||||
// sync_data was called a first time without filter, event from google calendar was created twice
|
||||
assert.containsN(target, '.fc-event-container', 4, "should display 4 events on the month");
|
||||
|
||||
await click(target.querySelector('.o_calendar_button_next'));
|
||||
await click(target.querySelector('.o_calendar_button_prev'));
|
||||
|
||||
assert.verifySteps([
|
||||
'/google_calendar/sync_data',
|
||||
'/web/dataset/call_kw/calendar.event/search_read',
|
||||
'/google_calendar/sync_data',
|
||||
'/web/dataset/call_kw/calendar.event/search_read',
|
||||
'/google_calendar/sync_data',
|
||||
'/web/dataset/call_kw/calendar.event/search_read',
|
||||
'/google_calendar/sync_data',
|
||||
'/web/dataset/call_kw/calendar.event/search_read',
|
||||
], 'should do a search_read before and after the call to sync_data');
|
||||
|
||||
assert.containsN(target, '.fc-event-container', 6, "should now display 6 events on the month");
|
||||
});
|
||||
|
||||
QUnit.test("component is destroyed while sync google calendar", async function (assert) {
|
||||
assert.expect(4);
|
||||
const def = makeDeferred();
|
||||
serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "Partners",
|
||||
res_model: "calendar.event",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "calendar"],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
serverData.views = {
|
||||
"calendar.event,false,calendar": `
|
||||
<calendar class="o_calendar_test" js_class="attendee_calendar" date_start="start" date_stop="stop">
|
||||
<field name="name"/>
|
||||
<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>
|
||||
</calendar>`,
|
||||
"calendar.event,false,list": `<tree sample="1"/>`,
|
||||
"calendar.event,false,search": `<search />`,
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({
|
||||
serverData,
|
||||
async mockRPC(route, args) {
|
||||
if (route === '/google_calendar/sync_data') {
|
||||
assert.step(route);
|
||||
return def;
|
||||
} else if (route === '/web/dataset/call_kw/calendar.event/search_read') {
|
||||
assert.step(route);
|
||||
} else if (route === '/web/dataset/call_kw/res.partner/get_attendee_detail') {
|
||||
return Promise.resolve([]);
|
||||
} else if (route === '/web/dataset/call_kw/res.users/has_group') {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await doAction(webClient, 1);
|
||||
|
||||
click(target.querySelector(".o_cp_switch_buttons .o_calendar"));
|
||||
await nextTick();
|
||||
|
||||
click(target.querySelector(".o_cp_switch_buttons .o_calendar"));
|
||||
await nextTick();
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([
|
||||
"/google_calendar/sync_data",
|
||||
"/google_calendar/sync_data",
|
||||
"/web/dataset/call_kw/calendar.event/search_read"
|
||||
], "Correct RPC calls were made");
|
||||
});
|
||||
});
|
||||