[IMP] mail, *: split mock server by model/controller
*: bus, calendar, hr_holidays, im_livechat, note, rating, snailmail, website_livechat. The mail mock server is getting bigger and bigger. As a consequence, navigating/keeping it up to date is quiet inconvenient. This commit solves this issue by splitting it by model/controller. closes odoo/odoo#98374 Related: odoo/enterprise#32314 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
@@ -104,9 +104,6 @@ let pyEnv;
|
||||
}
|
||||
pyEnv = new Proxy(
|
||||
{
|
||||
get currentPartner() {
|
||||
return this.mockServer.currentPartner;
|
||||
},
|
||||
getData() {
|
||||
return this.mockServer.models;
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@ patch(MockServer.prototype, 'bus', {
|
||||
this._super(message);
|
||||
},
|
||||
});
|
||||
this.pendingLongpollingPromise = null;
|
||||
this.notificationsToBeResolved = [];
|
||||
this.lastBusNotificationId = 0;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { MockServer } from '@web/../tests/helpers/mock_server';
|
||||
|
||||
patch(MockServer.prototype, 'calendar/models/calendar_event', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
// calendar.event methods
|
||||
if (args.model === 'calendar.event' && args.method === 'check_access_rights') {
|
||||
return true;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
// ensure mail override is applied first.
|
||||
import '@mail/../tests/helpers/mock_server/models/mail_activity';
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { MockServer } from '@web/../tests/helpers/mock_server';
|
||||
|
||||
patch(MockServer.prototype, 'calendar/models/mail_activity', {
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'mail.activity' && args.method === 'action_create_calendar_event') {
|
||||
return {
|
||||
type: 'ir.actions.act_window',
|
||||
name: "Meetings",
|
||||
res_model: 'calendar.event',
|
||||
view_mode: 'calendar',
|
||||
views: [[false, 'calendar']],
|
||||
target: 'current',
|
||||
};
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
+2
-32
@@ -1,44 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
// ensure mail override is applied first.
|
||||
import '@mail/../tests/helpers/mock_server';
|
||||
import '@mail/../tests/helpers/mock_server/models/res_users';
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { MockServer } from '@web/../tests/helpers/mock_server';
|
||||
|
||||
import { datetime_to_str } from 'web.time';
|
||||
|
||||
patch(MockServer.prototype, 'calendar', {
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
// mail.activity methods
|
||||
if (args.model === 'mail.activity' && args.method === 'action_create_calendar_event') {
|
||||
return {
|
||||
type: 'ir.actions.act_window',
|
||||
name: "Meetings",
|
||||
res_model: 'calendar.event',
|
||||
view_mode: 'calendar',
|
||||
views: [[false, 'calendar']],
|
||||
target: 'current',
|
||||
};
|
||||
}
|
||||
// calendar.event methods
|
||||
if (args.model === 'calendar.event' && args.method === 'check_access_rights') {
|
||||
return true;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private Mocked Methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
patch(MockServer.prototype, 'calendar/models/res_users', {
|
||||
/**
|
||||
* Simulates `_systray_get_calendar_event_domain` on `res.users`.
|
||||
*
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
|
||||
import '@mail/../tests/helpers/mock_server/models/res_partner'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'hr_holidays', {
|
||||
patch(MockServer.prototype, 'hr_holidays/models/res_partner', {
|
||||
/**
|
||||
* Overrides to add out of office to employees.
|
||||
*
|
||||
@@ -1,198 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'im_livechat', {
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/im_livechat/get_session') {
|
||||
const channel_id = args.channel_id;
|
||||
const anonymous_name = args.anonymous_name;
|
||||
const previous_operator_id = args.previous_operator_id;
|
||||
const context = args.context;
|
||||
return this._mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private Mocked Routes
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simulates the `/im_livechat/get_session` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channel_id
|
||||
* @param {string} anonymous_name
|
||||
* @param {integer} [previous_operator_id]
|
||||
* @param {Object} [context={}]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context = {}) {
|
||||
let user_id;
|
||||
let country_id;
|
||||
if ('mockedUserId' in context) {
|
||||
// can be falsy to simulate not being logged in
|
||||
user_id = context.mockedUserId;
|
||||
} else {
|
||||
user_id = this.currentUserId;
|
||||
}
|
||||
// don't use the anonymous name if the user is logged in
|
||||
if (user_id) {
|
||||
const user = this.getRecords('res.users', [['id', '=', user_id]])[0];
|
||||
country_id = user.country_id;
|
||||
} else {
|
||||
// simulate geoip
|
||||
const countryCode = context.mockedCountryCode;
|
||||
const country = this.getRecords('res.country', [['code', '=', countryCode]])[0];
|
||||
if (country) {
|
||||
country_id = country.id;
|
||||
anonymous_name = anonymous_name + ' (' + country.name + ')';
|
||||
}
|
||||
}
|
||||
return this._mockImLivechatChannel_openLivechatMailChannel(channel_id, anonymous_name, previous_operator_id, user_id, country_id);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private Mocked Methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_mockMailChannelChannelInfo(ids) {
|
||||
const channelInfos = this._super(...arguments);
|
||||
for (const channelInfo of channelInfos) {
|
||||
const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
|
||||
channelInfo['channel']['anonymous_name'] = channel.anonymous_name;
|
||||
// add the last message date
|
||||
if (channel.channel_type === 'livechat') {
|
||||
// add the operator id
|
||||
if (channel.livechat_operator_id) {
|
||||
const operator = this.getRecords('res.partner', [['id', '=', channel.livechat_operator_id]])[0];
|
||||
// livechat_username ignored for simplicity
|
||||
channelInfo.operator_pid = [operator.id, operator.display_name.replace(',', '')];
|
||||
}
|
||||
}
|
||||
}
|
||||
return channelInfos;
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_available_users` on `im_livechat.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockImLivechatChannel_getAvailableUsers(id) {
|
||||
const livechatChannel = this.getRecords('im_livechat.channel', [['id', '=', id]])[0];
|
||||
const users = this.getRecords('res.users', [['id', 'in', livechatChannel.user_ids]]);
|
||||
return users.filter(user => user.im_status === 'online');
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_livechat_mail_channel_vals` on `im_livechat.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id) {
|
||||
// partner to add to the mail.channel
|
||||
const operator_partner_id = operator.partner_id;
|
||||
const membersToAdd = [[0, 0, {
|
||||
is_pinned: false,
|
||||
partner_id: operator_partner_id,
|
||||
}]];
|
||||
let visitor_user;
|
||||
if (user_id) {
|
||||
const visitor_user = this.getRecords('res.users', [['id', '=', user_id]])[0];
|
||||
if (visitor_user && visitor_user.active && visitor_user !== operator) {
|
||||
// valid session user (not public)
|
||||
membersToAdd.push([0, 0, { partner_id: visitor_user.partner_id.id }]);
|
||||
}
|
||||
} else {
|
||||
membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
|
||||
}
|
||||
const membersName = [
|
||||
visitor_user ? visitor_user.display_name : anonymous_name,
|
||||
operator.livechat_username ? operator.livechat_username : operator.name,
|
||||
];
|
||||
return {
|
||||
'channel_member_ids': membersToAdd,
|
||||
'livechat_active': true,
|
||||
'livechat_operator_id': operator_partner_id,
|
||||
'livechat_channel_id': id,
|
||||
'anonymous_name': user_id ? false : anonymous_name,
|
||||
'country_id': country_id,
|
||||
'channel_type': 'livechat',
|
||||
'name': membersName.join(' '),
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_random_operator` on `im_livechat.channel`.
|
||||
* Simplified mock implementation: returns the first available operator.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockImLivechatChannel_getRandomOperator(id) {
|
||||
const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
|
||||
return availableUsers[0];
|
||||
},
|
||||
/**
|
||||
* Simulates `_open_livechat_mail_channel` on `im_livechat.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @param {string} anonymous_name
|
||||
* @param {integer} [previous_operator_id]
|
||||
* @param {integer} [user_id]
|
||||
* @param {integer} [country_id]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockImLivechatChannel_openLivechatMailChannel(id, anonymous_name, previous_operator_id, user_id, country_id) {
|
||||
let operator;
|
||||
if (previous_operator_id) {
|
||||
const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
|
||||
operator = availableUsers.find(user => user.partner_id === previous_operator_id);
|
||||
}
|
||||
if (!operator) {
|
||||
operator = this._mockImLivechatChannel_getRandomOperator(id);
|
||||
}
|
||||
if (!operator) {
|
||||
// no one available
|
||||
return false;
|
||||
}
|
||||
// create the session, and add the link with the given channel
|
||||
const mailChannelVals = this._mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id);
|
||||
const mailChannelId = this.pyEnv['mail.channel'].create(mailChannelVals);
|
||||
this._mockMailChannel_broadcast([mailChannelId], [operator.partner_id]);
|
||||
return this._mockMailChannelChannelInfo([mailChannelId])[0];
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_mockResPartner_GetChannelsAsMember(ids) {
|
||||
const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0];
|
||||
const members = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]);
|
||||
const livechats = this.getRecords('mail.channel', [
|
||||
['channel_type', '=', 'livechat'],
|
||||
['channel_member_ids', 'in', members.map(member => member.id)],
|
||||
]);
|
||||
return [
|
||||
...this._super(ids),
|
||||
...livechats,
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,5 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
@@ -10,6 +8,13 @@ patch(MockServer.prototype, 'im_livechat/controllers/main', {
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/im_livechat/get_session') {
|
||||
const channel_id = args.channel_id;
|
||||
const anonymous_name = args.anonymous_name;
|
||||
const previous_operator_id = args.previous_operator_id;
|
||||
const context = args.context;
|
||||
return this._mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context);
|
||||
}
|
||||
if (route === '/im_livechat/notify_typing') {
|
||||
const uuid = args.uuid;
|
||||
const is_typing = args.is_typing;
|
||||
@@ -18,6 +23,40 @@ patch(MockServer.prototype, 'im_livechat/controllers/main', {
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/im_livechat/get_session` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channel_id
|
||||
* @param {string} anonymous_name
|
||||
* @param {integer} [previous_operator_id]
|
||||
* @param {Object} [context={}]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteImLivechatGetSession(channel_id, anonymous_name, previous_operator_id, context = {}) {
|
||||
let user_id;
|
||||
let country_id;
|
||||
if ('mockedUserId' in context) {
|
||||
// can be falsy to simulate not being logged in
|
||||
user_id = context.mockedUserId;
|
||||
} else {
|
||||
user_id = this.currentUserId;
|
||||
}
|
||||
// don't use the anonymous name if the user is logged in
|
||||
if (user_id) {
|
||||
const user = this.getRecords('res.users', [['id', '=', user_id]])[0];
|
||||
country_id = user.country_id;
|
||||
} else {
|
||||
// simulate geoip
|
||||
const countryCode = context.mockedCountryCode;
|
||||
const country = this.getRecords('res.country', [['code', '=', countryCode]])[0];
|
||||
if (country) {
|
||||
country_id = country.id;
|
||||
anonymous_name = anonymous_name + ' (' + country.name + ')';
|
||||
}
|
||||
}
|
||||
return this._mockImLivechatChannel_openLivechatMailChannel(channel_id, anonymous_name, previous_operator_id, user_id, country_id);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/im_livechat/notify_typing` route.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'im_livechat/models/im_livechat_channel', {
|
||||
/**
|
||||
* Simulates `_get_available_users` on `im_livechat.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockImLivechatChannel_getAvailableUsers(id) {
|
||||
const livechatChannel = this.getRecords('im_livechat.channel', [['id', '=', id]])[0];
|
||||
const users = this.getRecords('res.users', [['id', 'in', livechatChannel.user_ids]]);
|
||||
return users.filter(user => user.im_status === 'online');
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_livechat_mail_channel_vals` on `im_livechat.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id) {
|
||||
// partner to add to the mail.channel
|
||||
const operator_partner_id = operator.partner_id;
|
||||
const membersToAdd = [[0, 0, {
|
||||
is_pinned: false,
|
||||
partner_id: operator_partner_id,
|
||||
}]];
|
||||
let visitor_user;
|
||||
if (user_id) {
|
||||
const visitor_user = this.getRecords('res.users', [['id', '=', user_id]])[0];
|
||||
if (visitor_user && visitor_user.active && visitor_user !== operator) {
|
||||
// valid session user (not public)
|
||||
membersToAdd.push([0, 0, { partner_id: visitor_user.partner_id.id }]);
|
||||
}
|
||||
} else {
|
||||
membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
|
||||
}
|
||||
const membersName = [
|
||||
visitor_user ? visitor_user.display_name : anonymous_name,
|
||||
operator.livechat_username ? operator.livechat_username : operator.name,
|
||||
];
|
||||
return {
|
||||
'channel_member_ids': membersToAdd,
|
||||
'livechat_active': true,
|
||||
'livechat_operator_id': operator_partner_id,
|
||||
'livechat_channel_id': id,
|
||||
'anonymous_name': user_id ? false : anonymous_name,
|
||||
'country_id': country_id,
|
||||
'channel_type': 'livechat',
|
||||
'name': membersName.join(' '),
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_random_operator` on `im_livechat.channel`.
|
||||
* Simplified mock implementation: returns the first available operator.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockImLivechatChannel_getRandomOperator(id) {
|
||||
const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
|
||||
return availableUsers[0];
|
||||
},
|
||||
/**
|
||||
* Simulates `_open_livechat_mail_channel` on `im_livechat.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @param {string} anonymous_name
|
||||
* @param {integer} [previous_operator_id]
|
||||
* @param {integer} [user_id]
|
||||
* @param {integer} [country_id]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockImLivechatChannel_openLivechatMailChannel(id, anonymous_name, previous_operator_id, user_id, country_id) {
|
||||
let operator;
|
||||
if (previous_operator_id) {
|
||||
const availableUsers = this._mockImLivechatChannel_getAvailableUsers(id);
|
||||
operator = availableUsers.find(user => user.partner_id === previous_operator_id);
|
||||
}
|
||||
if (!operator) {
|
||||
operator = this._mockImLivechatChannel_getRandomOperator(id);
|
||||
}
|
||||
if (!operator) {
|
||||
// no one available
|
||||
return false;
|
||||
}
|
||||
// create the session, and add the link with the given channel
|
||||
const mailChannelVals = this._mockImLivechatChannel_getLivechatMailChannelVals(id, anonymous_name, operator, user_id, country_id);
|
||||
const mailChannelId = this.pyEnv['mail.channel'].create(mailChannelVals);
|
||||
this._mockMailChannel_broadcast([mailChannelId], [operator.partner_id]);
|
||||
return this._mockMailChannelChannelInfo([mailChannelId])[0];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@mail/../tests/helpers/mock_server/models/mail_channel'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'im_livechat/models/mail_channel', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_mockMailChannelChannelInfo(ids) {
|
||||
const channelInfos = this._super(...arguments);
|
||||
for (const channelInfo of channelInfos) {
|
||||
const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
|
||||
channelInfo['channel']['anonymous_name'] = channel.anonymous_name;
|
||||
// add the last message date
|
||||
if (channel.channel_type === 'livechat') {
|
||||
// add the operator id
|
||||
if (channel.livechat_operator_id) {
|
||||
const operator = this.getRecords('res.partner', [['id', '=', channel.livechat_operator_id]])[0];
|
||||
// livechat_username ignored for simplicity
|
||||
channelInfo.operator_pid = [operator.id, operator.display_name.replace(',', '')];
|
||||
}
|
||||
}
|
||||
}
|
||||
return channelInfos;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@mail/../tests/helpers/mock_server/models/res_partner'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'im_livechat/models/res_partner', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_mockResPartner_GetChannelsAsMember(ids) {
|
||||
const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0];
|
||||
const members = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]);
|
||||
const livechats = this.getRecords('mail.channel', [
|
||||
['channel_type', '=', 'livechat'],
|
||||
['channel_member_ids', 'in', members.map(member => member.id)],
|
||||
]);
|
||||
return [
|
||||
...this._super(ids),
|
||||
...livechats,
|
||||
];
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/controllers/bus', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/mail/chat_post') {
|
||||
const uuid = args.uuid;
|
||||
const message_content = args.message_content;
|
||||
const context = args.context;
|
||||
return this._mockRouteMailChatPost(uuid, message_content, context);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/chat_post` route.
|
||||
*
|
||||
* @private
|
||||
* @param {string} uuid
|
||||
* @param {string} message_content
|
||||
* @param {Object} [context={}]
|
||||
* @returns {Object} one key for list of followers and one for subtypes
|
||||
*/
|
||||
async _mockRouteMailChatPost(uuid, message_content, context = {}) {
|
||||
const mailChannel = this.getRecords('mail.channel', [['uuid', '=', uuid]])[0];
|
||||
if (!mailChannel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let user_id;
|
||||
// find the author from the user session
|
||||
if ('mockedUserId' in context) {
|
||||
// can be falsy to simulate not being logged in
|
||||
user_id = context.mockedUserId;
|
||||
} else {
|
||||
user_id = this.currentUserId;
|
||||
}
|
||||
let author_id;
|
||||
let email_from;
|
||||
if (user_id) {
|
||||
const author = this.getRecords('res.users', [['id', '=', user_id]])[0];
|
||||
author_id = author.partner_id;
|
||||
email_from = `${author.display_name} <${author.email}>`;
|
||||
} else {
|
||||
author_id = false;
|
||||
// simpler fallback than catchall_formatted
|
||||
email_from = mailChannel.anonymous_name || "catchall@example.com";
|
||||
}
|
||||
// supposedly should convert plain text to html
|
||||
const body = message_content;
|
||||
// ideally should be posted with mail_create_nosubscribe=True
|
||||
return this._mockMailChannelMessagePost(
|
||||
mailChannel.id,
|
||||
{
|
||||
author_id,
|
||||
email_from,
|
||||
body,
|
||||
message_type: 'comment',
|
||||
subtype_xmlid: 'mail.mt_comment',
|
||||
},
|
||||
context
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -4,19 +4,83 @@ import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/controllers/discuss', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async performRPC(route, args) {
|
||||
if (route === '/mail/attachment/upload') {
|
||||
const ufile = args.body.get('ufile');
|
||||
const is_pending = args.body.get('is_pending') === 'true';
|
||||
const model = is_pending ? 'mail.compose.message' : args.body.get('thread_model');
|
||||
const id = is_pending ? 0 : parseInt(args.body.get('thread_id'));
|
||||
const attachmentId = this.mockCreate('ir.attachment', {
|
||||
// datas,
|
||||
mimetype: ufile.type,
|
||||
name: ufile.name,
|
||||
res_id: id,
|
||||
res_model: model,
|
||||
});
|
||||
const attachment = this.getRecords('ir.attachment', [['id', '=', attachmentId]])[0];
|
||||
return {
|
||||
'filename': attachment.name,
|
||||
'id': attachment.id,
|
||||
'mimetype': attachment.mimetype,
|
||||
'size': attachment.file_size
|
||||
};
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/mail/attachment/delete') {
|
||||
const { attachment_id } = args;
|
||||
return this._mockRouteMailAttachmentRemove(attachment_id);
|
||||
}
|
||||
if (route === '/mail/channel/messages') {
|
||||
const { channel_id, min_id, max_id, limit } = args;
|
||||
return this._mockRouteMailChannelMessages(channel_id, max_id, min_id, limit);
|
||||
}
|
||||
if (route === '/mail/channel/notify_typing') {
|
||||
const id = args.channel_id;
|
||||
const is_typing = args.is_typing;
|
||||
const context = args.context;
|
||||
return this._mockRouteMailChannelNotifyTyping(id, is_typing, context);
|
||||
}
|
||||
if (new RegExp('/mail/channel/\\d+/partner/\\d+/avatar_128').test(route)) {
|
||||
return;
|
||||
}
|
||||
if (route === '/mail/channel/ping') {
|
||||
return;
|
||||
}
|
||||
if (route === '/mail/history/messages') {
|
||||
const { min_id, max_id, limit } = args;
|
||||
return this._mockRouteMailMessageHistory(min_id, max_id, limit);
|
||||
}
|
||||
if (route === '/mail/init_messaging') {
|
||||
return this._mockRouteMailInitMessaging();
|
||||
}
|
||||
if (route === '/mail/inbox/messages') {
|
||||
const { min_id, max_id, limit } = args;
|
||||
return this._mockRouteMailMessageInbox(min_id, max_id, limit);
|
||||
}
|
||||
if (route === '/mail/link_preview') {
|
||||
return this._mockRouteMailLinkPreview(args.message_id);
|
||||
}
|
||||
if (route === '/mail/load_message_failures') {
|
||||
return this._mockRouteMailLoadMessageFailures();
|
||||
}
|
||||
if (route === '/mail/message/post') {
|
||||
if (args.thread_model === 'mail.channel') {
|
||||
return this._mockMailChannelMessagePost(args.thread_id, args.post_data, args.context);
|
||||
}
|
||||
return this._mockMailThreadMessagePost(args.thread_model, [args.thread_id], args.post_data, args.context);
|
||||
}
|
||||
if (route === '/mail/read_subscription_data') {
|
||||
const follower_id = args.follower_id;
|
||||
return this._mockRouteMailReadSubscriptionData(follower_id);
|
||||
}
|
||||
if (route === '/mail/rtc/channel/join_call') {
|
||||
return this._mockRouteMailRtcChannelJoinCall(args.channel_id, args.check_rtc_session_ids);
|
||||
}
|
||||
@@ -26,8 +90,58 @@ patch(MockServer.prototype, 'mail/controllers/discuss', {
|
||||
if (route === '/mail/rtc/session/update_and_broadcast') {
|
||||
return;
|
||||
}
|
||||
if (route === '/mail/starred/messages') {
|
||||
const { min_id, max_id, limit } = args;
|
||||
return this._mockRouteMailMessageStarredMessages(min_id, max_id, limit);
|
||||
}
|
||||
if (route === '/mail/thread/data') {
|
||||
return this._mockRouteMailThreadData(args.thread_model, args.thread_id, args.request_list);
|
||||
}
|
||||
if (route === '/mail/thread/messages') {
|
||||
const { min_id, max_id, limit, thread_model, thread_id } = args;
|
||||
return this._mockRouteMailThreadFetchMessages(thread_model, thread_id, max_id, min_id, limit);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/init_messaging` route.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteMailInitMessaging() {
|
||||
return this._mockResUsers_InitMessaging([this.currentUserId]);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/attachment/delete` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} attachment_id
|
||||
*/
|
||||
async _mockRouteMailAttachmentRemove(attachment_id) {
|
||||
this.pyEnv['bus.bus']._sendone(this.currentPartnerId, 'ir.attachment/delete', { id: attachment_id });
|
||||
return this.pyEnv['ir.attachment'].unlink([attachment_id]);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/channel/messages` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channel_id
|
||||
* @param {integer} limit
|
||||
* @param {integer} max_id
|
||||
* @param {integer} min_id
|
||||
* @returns {Object} list of messages
|
||||
*/
|
||||
async _mockRouteMailChannelMessages(channel_id, max_id = false, min_id = false, limit = 30) {
|
||||
const domain = [
|
||||
['res_id', '=', channel_id],
|
||||
['model', '=', 'mail.channel'],
|
||||
['message_type', '!=', 'user_notification'],
|
||||
];
|
||||
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
|
||||
this._mockMailMessageSetMessageDone(messages.map(message => message.id));
|
||||
return this._mockMailMessageMessageFormat(messages.map(message => message.id));
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/channel/notify_typing` route.
|
||||
*
|
||||
@@ -41,6 +155,108 @@ patch(MockServer.prototype, 'mail/controllers/discuss', {
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerId]]);
|
||||
this._mockMailChannelMember_NotifyTyping([memberOfCurrentUser.id], is_typing);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/load_message_failures` route.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockRouteMailLoadMessageFailures() {
|
||||
return this._mockResPartner_MessageFetchFailed(this.currentPartnerId);
|
||||
},
|
||||
/**
|
||||
* Simulates `/mail/link_preview` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} message_id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteMailLinkPreview(message_id) {
|
||||
const linkPreviews = [];
|
||||
const [message] = this.pyEnv['mail.message'].searchRead([['id', '=', message_id]]);
|
||||
if (message.body === 'https://make-link-preview.com') {
|
||||
const linkPreviewId = this.pyEnv['mail.link.preview'].create({
|
||||
og_description: "test description",
|
||||
og_title: 'Article title',
|
||||
og_type: 'article',
|
||||
source_url: 'https://make-link-preview.com',
|
||||
});
|
||||
const [linkPreview] = this.pyEnv['mail.link.preview'].searchRead([['id', '=', linkPreviewId]]);
|
||||
linkPreviews.push(this._mockMailLinkPreviewFormat(linkPreview));
|
||||
let target = this.pyEnv.currentPartner;
|
||||
if (message.model === 'mail.channel') {
|
||||
target = this.pyEnv['mail.channel'].search([['id', '=', message.res_id]]);
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendmany([[target, 'mail.link.preview/insert', linkPreviews]]);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/history/messages` route.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteMailMessageHistory(min_id = false, max_id = false, limit = 30) {
|
||||
const domain = [['needaction', '=', false]];
|
||||
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
|
||||
return this._mockMailMessageMessageFormat(messages.map(message => message.id));
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/inbox/messages` route.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteMailMessageInbox(min_id = false, max_id = false, limit = 30) {
|
||||
const domain = [['needaction', '=', true]];
|
||||
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
|
||||
return this._mockMailMessageMessageFormat(messages.map(message => message.id));
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/starred/messages` route.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteMailMessageStarredMessages(min_id = false, max_id = false, limit = 30) {
|
||||
const domain = [['starred_partner_ids', 'in', [this.currentPartnerId]]];
|
||||
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
|
||||
return this._mockMailMessageMessageFormat(messages.map(message => message.id));
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/read_subscription_data` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} follower_id
|
||||
* @returns {Object[]} list of followed subtypes
|
||||
*/
|
||||
async _mockRouteMailReadSubscriptionData(follower_id) {
|
||||
const follower = this.getRecords('mail.followers', [['id', '=', follower_id]])[0];
|
||||
const subtypes = this.getRecords('mail.message.subtype', [
|
||||
'&',
|
||||
['hidden', '=', false],
|
||||
'|',
|
||||
['res_model', '=', follower.res_model],
|
||||
['res_model', '=', false],
|
||||
]);
|
||||
const subtypes_list = subtypes.map(subtype => {
|
||||
const parent = this.getRecords('mail.message.subtype', [
|
||||
['id', '=', subtype.parent_id],
|
||||
])[0];
|
||||
return {
|
||||
'default': subtype.default,
|
||||
'followed': follower.subtype_ids.includes(subtype.id),
|
||||
'id': subtype.id,
|
||||
'internal': subtype.internal,
|
||||
'name': subtype.name,
|
||||
'parent_model': parent ? parent.res_model : false,
|
||||
'res_model': subtype.res_model,
|
||||
'sequence': subtype.sequence,
|
||||
};
|
||||
});
|
||||
// NOTE: server is also doing a sort here, not reproduced for simplicity
|
||||
return subtypes_list;
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/rtc/channel/join_call` route.
|
||||
*
|
||||
@@ -105,4 +321,66 @@ patch(MockServer.prototype, 'mail/controllers/discuss', {
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendmany(notifications);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/thread/data` route.
|
||||
*
|
||||
* @param {string} thread_model
|
||||
* @param {integer} thread_id
|
||||
* @param {string[]} request_list
|
||||
* @returns {Object}
|
||||
*/
|
||||
async _mockRouteMailThreadData(thread_model, thread_id, request_list) {
|
||||
const res = {
|
||||
'hasWriteAccess': true, // mimic user with write access by default
|
||||
'hasReadAccess': true,
|
||||
};
|
||||
const thread = this.pyEnv[thread_model].searchRead([['id', '=', thread_id]])[0];
|
||||
if (!thread) {
|
||||
res['hasReadAccess'] = false;
|
||||
return res;
|
||||
}
|
||||
if (request_list.includes('activities')) {
|
||||
const activities = this.pyEnv['mail.activity'].searchRead([['id', 'in', thread.activity_ids || []]]);
|
||||
res['activities'] = this._mockMailActivityActivityFormat(activities.map(activity => activity.id));
|
||||
}
|
||||
if (request_list.includes('attachments')) {
|
||||
const attachments = this.pyEnv['ir.attachment'].searchRead(
|
||||
[['res_id', '=', thread.id], ['res_model', '=', thread_model]],
|
||||
); // order not done for simplicity
|
||||
res['attachments'] = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id));
|
||||
res['mainAttachment'] = thread.message_main_attachment_id ? { 'id': thread.message_main_attachment_id[0] } : [['clear']];
|
||||
}
|
||||
if (request_list.includes('followers')) {
|
||||
const followers = this.pyEnv['mail.followers'].searchRead([['id', 'in', thread.message_follower_ids || []]]);
|
||||
// search read returns many2one relations as an array [id, display_name].
|
||||
// But the original route does not. Thus, we need to change it now.
|
||||
followers.forEach(follower => follower.partner_id = follower.partner_id[0]);
|
||||
res['followers'] = followers;
|
||||
}
|
||||
if (request_list.includes('suggestedRecipients')) {
|
||||
res['suggestedRecipients'] = this._mockMailThread_MessageGetSuggestedRecipients(thread_model, [thread.id])[thread_id];
|
||||
}
|
||||
return res;
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/thread/messages` route.
|
||||
*
|
||||
* @private
|
||||
* @param {string} res_model
|
||||
* @param {integer} res_id
|
||||
* @param {integer} max_id
|
||||
* @param {integer} min_id
|
||||
* @param {integer} limit
|
||||
* @returns {Object[]} list of messages
|
||||
*/
|
||||
async _mockRouteMailThreadFetchMessages(res_model, res_id, max_id = false, min_id = false, limit = 30) {
|
||||
const domain = [
|
||||
['res_id', '=', res_id],
|
||||
['model', '=', res_model],
|
||||
['message_type', '!=', 'user_notification'],
|
||||
];
|
||||
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
|
||||
this._mockMailMessageSetMessageDone(messages.map(message => message.id));
|
||||
return this._mockMailMessageMessageFormat(messages.map(message => message.id));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/ir_attachment', {
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'ir.attachment' && args.method === 'register_as_main_attachment') {
|
||||
const ids = args.args[0];
|
||||
return this._mockIrAttachmentRegisterAsMainAttachment(ids);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates `_attachment_format` on `ir.attachment`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockIrAttachment_attachmentFormat(ids) {
|
||||
const attachments = this.mockRead('ir.attachment', [ids]);
|
||||
return attachments.map(attachment => {
|
||||
const res = {
|
||||
'checksum': attachment.checksum,
|
||||
'filename': attachment.name,
|
||||
'id': attachment.id,
|
||||
'mimetype': attachment.mimetype,
|
||||
'name': attachment.name,
|
||||
};
|
||||
res['originThread'] = [['insert', {
|
||||
'id': attachment.res_id,
|
||||
'model': attachment.res_model,
|
||||
}]];
|
||||
return res;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates `register_as_main_attachment` on `ir.attachment`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} ids
|
||||
* @param {boolean} [force=true]
|
||||
* @returns {boolean} dummy value for mock server
|
||||
*/
|
||||
_mockIrAttachmentRegisterAsMainAttachment(ids, force = true) {
|
||||
const [attachment] = this.getRecords('ir.attachment', [['id', 'in', ids]]);
|
||||
if (!attachment.res_model) {
|
||||
return true; // dummy value for mock server
|
||||
}
|
||||
if (!this.models[attachment.res_model].fields['message_main_attachment_id']) {
|
||||
return true; // dummy value for mock server
|
||||
}
|
||||
const [record] = this.pyEnv[attachment.res_model].searchRead([['id', '=', attachment.res_id]]);
|
||||
if (force || !record.message_main_attachment_id) {
|
||||
this.pyEnv[attachment.res_model].write(
|
||||
[record.id],
|
||||
{ message_main_attachment_id: attachment.id },
|
||||
);
|
||||
}
|
||||
return true; // dummy value for mock server
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_activity', {
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'mail.activity' && args.method === 'action_feedback') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailActivityActionFeedback(ids);
|
||||
}
|
||||
if (args.model === 'mail.activity' && args.method === 'action_feedback_schedule_next') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailActivityActionFeedbackScheduleNext(ids);
|
||||
}
|
||||
if (args.model === 'mail.activity' && args.method === 'activity_format') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailActivityActivityFormat(ids);
|
||||
}
|
||||
if (args.model === 'mail.activity' && args.method === 'get_activity_data') {
|
||||
const res_model = args.args[0] || args.kwargs.res_model;
|
||||
const domain = args.args[1] || args.kwargs.domain;
|
||||
return this._mockMailActivityGetActivityData(res_model, domain);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates `activity_format` on `mail.activity`.
|
||||
*
|
||||
* @private
|
||||
* @param {number[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailActivityActivityFormat(ids) {
|
||||
let res = this.mockRead('mail.activity', [ids]);
|
||||
res = res.map(record => {
|
||||
if (record.mail_template_ids) {
|
||||
record.mail_template_ids = record.mail_template_ids.map(template_id => {
|
||||
const template = this.getRecords('mail.template', [['id', '=', template_id]])[0];
|
||||
return {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
return record;
|
||||
});
|
||||
return res;
|
||||
},
|
||||
/**
|
||||
* Simulates `_action_done` on `mail.activity`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailActivityActionDone(ids) {
|
||||
const activities = this.getRecords('mail.activity', [['id', 'in', ids]]);
|
||||
this.mockUnlink('mail.activity', [activities.map(activity => activity.id)]);
|
||||
},
|
||||
/**
|
||||
* Simulates `action_feedback` on `mail.activity`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailActivityActionFeedback(ids) {
|
||||
this._mockMailActivityActionDone(ids);
|
||||
},
|
||||
/**
|
||||
* Simulates `action_feedback_schedule_next` on `mail.activity`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailActivityActionFeedbackScheduleNext(ids) {
|
||||
this._mockMailActivityActionDone(ids);
|
||||
return {
|
||||
name: 'Schedule an Activity',
|
||||
view_mode: 'form',
|
||||
res_model: 'mail.activity',
|
||||
views: [[false, 'form']],
|
||||
type: 'ir.actions.act_window',
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates `get_activity_data` on `mail.activity`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} res_model
|
||||
* @param {string} domain
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailActivityGetActivityData(res_model, domain) {
|
||||
const self = this;
|
||||
const records = this.getRecords(res_model, domain);
|
||||
|
||||
const activityTypes = this.getRecords('mail.activity.type', []);
|
||||
const activityIds = _.pluck(records, 'activity_ids').flat();
|
||||
|
||||
const groupedActivities = {};
|
||||
const resIdToDeadline = {};
|
||||
const groups = self.mockReadGroup('mail.activity', {
|
||||
domain: [['id', 'in', activityIds]],
|
||||
fields: ['res_id', 'activity_type_id', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'],
|
||||
groupby: ['res_id', 'activity_type_id'],
|
||||
lazy: false,
|
||||
});
|
||||
groups.forEach(function (group) {
|
||||
// mockReadGroup doesn't correctly return all asked fields
|
||||
const activites = self.getRecords('mail.activity', group.__domain);
|
||||
group.activity_type_id = group.activity_type_id[0];
|
||||
let minDate;
|
||||
activites.forEach(function (activity) {
|
||||
if (!minDate || moment(activity.date_deadline) < moment(minDate)) {
|
||||
minDate = activity.date_deadline;
|
||||
}
|
||||
});
|
||||
group.date_deadline = minDate;
|
||||
resIdToDeadline[group.res_id] = minDate;
|
||||
let state;
|
||||
if (group.date_deadline === moment().format("YYYY-MM-DD")) {
|
||||
state = 'today';
|
||||
} else if (moment(group.date_deadline) > moment()) {
|
||||
state = 'planned';
|
||||
} else {
|
||||
state = 'overdue';
|
||||
}
|
||||
if (!groupedActivities[group.res_id]) {
|
||||
groupedActivities[group.res_id] = {};
|
||||
}
|
||||
groupedActivities[group.res_id][group.activity_type_id] = {
|
||||
count: group.__count,
|
||||
state: state,
|
||||
o_closest_deadline: group.date_deadline,
|
||||
ids: _.pluck(activites, 'id'),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
activity_types: activityTypes.map((type) => {
|
||||
let mailTemplates = [];
|
||||
if (type.mail_template_ids) {
|
||||
mailTemplates = type.mail_template_ids.map((template_id) => {
|
||||
const template = this.getRecords('mail.template', [['id', '=', template_id]])[0];
|
||||
return {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
return [type.id, type.display_name, mailTemplates];
|
||||
}),
|
||||
activity_res_ids: _.sortBy(_.pluck(records, 'id'), function (id) {
|
||||
return moment(resIdToDeadline[id]);
|
||||
}),
|
||||
grouped_activities: groupedActivities,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,709 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
import { datetime_to_str } from 'web.time';
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_channel', {
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'mail.channel' && args.method === 'action_unfollow') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailChannelActionUnfollow(ids);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'channel_fetched') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailChannelChannelFetched(ids);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'channel_fetch_preview') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailChannelChannelFetchPreview(ids);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'channel_fold') {
|
||||
const ids = args.args[0];
|
||||
const state = args.args[1] || args.kwargs.state;
|
||||
return this._mockMailChannelChannelFold(ids, state);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'channel_get') {
|
||||
const partners_to = args.args[0] || args.kwargs.partners_to;
|
||||
const pin = args.args[1] !== undefined
|
||||
? args.args[1]
|
||||
: args.kwargs.pin !== undefined
|
||||
? args.kwargs.pin
|
||||
: undefined;
|
||||
return this._mockMailChannelChannelGet(partners_to, pin);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'channel_info') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailChannelChannelInfo(ids);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'add_members') {
|
||||
const ids = args.args[0];
|
||||
const partner_ids = args.args[1] || args.kwargs.partner_ids;
|
||||
return this._mockMailChannelAddMembers(ids, partner_ids);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'channel_pin') {
|
||||
const ids = args.args[0];
|
||||
const pinned = args.args[1] || args.kwargs.pinned;
|
||||
return this._mockMailChannelChannelPin(ids, pinned);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'channel_rename') {
|
||||
const ids = args.args[0];
|
||||
const name = args.args[1] || args.kwargs.name;
|
||||
return this._mockMailChannelChannelRename(ids, name);
|
||||
}
|
||||
if (route === '/mail/channel/set_last_seen_message') {
|
||||
const id = args.channel_id;
|
||||
const last_message_id = args.last_message_id;
|
||||
return this._mockMailChannel_ChannelSeen([id], last_message_id);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'channel_set_custom_name') {
|
||||
const ids = args.args[0];
|
||||
const name = args.args[1] || args.kwargs.name;
|
||||
return this._mockMailChannelChannelSetCustomName(ids, name);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'create_group') {
|
||||
const partners_to = args.args[0] || args.kwargs.partners_to;
|
||||
return this._mockMailChannelCreateGroup(partners_to);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'execute_command_leave') {
|
||||
return this._mockMailChannelExecuteCommandLeave(args);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'execute_command_who') {
|
||||
return this._mockMailChannelExecuteCommandWho(args);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'write' && 'image_128' in args.args[1]) {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailChannelWriteImage128(ids[0]);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'load_more_members') {
|
||||
const [channel_ids] = args.args;
|
||||
const { known_member_ids } = args.kwargs;
|
||||
return this._mockMailChannelLoadMoreMembers(channel_ids, known_member_ids);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'get_mention_suggestions') {
|
||||
return this._mockMailChannelGetMentionSuggestions(args);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates `message_post` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @param {Object} kwargs
|
||||
* @param {Object} [context]
|
||||
* @returns {integer|false}
|
||||
*/
|
||||
_mockMailChannelMessagePost(id, kwargs, context) {
|
||||
const message_type = kwargs.message_type || 'notification';
|
||||
const channel = this.getRecords('mail.channel', [['id', '=', id]])[0];
|
||||
if (channel.channel_type !== 'channel') {
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
|
||||
this.pyEnv['mail.channel.member'].write(
|
||||
[memberOfCurrentUser.id],
|
||||
{
|
||||
last_interest_dt: datetime_to_str(new Date()),
|
||||
is_pinned: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
const messageData = this._mockMailThreadMessagePost(
|
||||
'mail.channel',
|
||||
[id],
|
||||
Object.assign(kwargs, {
|
||||
message_type,
|
||||
}),
|
||||
context,
|
||||
);
|
||||
if (kwargs.author_id === this.currentPartnerId) {
|
||||
this._mockMailChannel_SetLastSeenMessage([channel.id], messageData.id);
|
||||
}
|
||||
// simulate compute of message_unread_counter
|
||||
const otherMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '!=', this.currentPartnerId]]);
|
||||
for (const member of otherMembers) {
|
||||
this.pyEnv['mail.channel.member'].write(
|
||||
[member.id],
|
||||
{ message_unread_counter: member.message_unread_counter + 1 },
|
||||
);
|
||||
}
|
||||
return messageData;
|
||||
},
|
||||
/**
|
||||
* Simulates `action_unfollow` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
*/
|
||||
_mockMailChannelActionUnfollow(ids) {
|
||||
const channel = this.getRecords('mail.channel', [['id', 'in', ids]])[0];
|
||||
const [channelMember] = this.getRecords('mail.channel.member', [['channel_id', 'in', ids], ['partner_id', '=', this.currentPartnerId]]);
|
||||
if (!channelMember) {
|
||||
return true;
|
||||
}
|
||||
this.pyEnv['mail.channel'].write(
|
||||
[channel.id],
|
||||
{
|
||||
channel_member_ids: [[2, channelMember.id]],
|
||||
},
|
||||
);
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/leave', {
|
||||
'id': channel.id,
|
||||
});
|
||||
/**
|
||||
* Leave message not posted here because it would send the new message
|
||||
* notification on a separate bus notification list from the unsubscribe
|
||||
* itself which would lead to the channel being pinned again (handler
|
||||
* for unsubscribe is weak and is relying on both of them to be sent
|
||||
* together on the bus).
|
||||
*/
|
||||
// this._mockMailChannelMessagePost(channel.id, {
|
||||
// author_id: this.currentPartnerId,
|
||||
// body: '<div class="o_mail_notification">left the channel</div>',
|
||||
// subtype_xmlid: "mail.mt_comment",
|
||||
// });
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* Simulates `add_members` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @param {integer[]} partner_ids
|
||||
*/
|
||||
_mockMailChannelAddMembers(ids, partner_ids) {
|
||||
const [channel] = this.getRecords('mail.channel', [['id', 'in', ids]]);
|
||||
const partners = this.getRecords('res.partner', [['id', 'in', partner_ids]]);
|
||||
for (const partner of partners) {
|
||||
this.pyEnv['mail.channel.member'].create({
|
||||
channel_id: channel.id,
|
||||
partner_id: partner.id,
|
||||
});
|
||||
const body = `<div class="o_mail_notification">invited ${partner.name} to the channel</div>`;
|
||||
const message_type = "notification";
|
||||
const subtype_xmlid = "mail.mt_comment";
|
||||
this._mockMailChannelMessagePost(
|
||||
channel.id,
|
||||
{ body, message_type, subtype_xmlid },
|
||||
);
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendone(channel, 'mail.channel/joined', {
|
||||
'channel': this._mockMailChannelChannelInfo([channel.id])[0],
|
||||
'invited_by_user_id': this.currentUserId,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates `_broadcast` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @param {integer[]} partner_ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailChannel_broadcast(ids, partner_ids) {
|
||||
const notifications = this._mockMailChannel_channelChannelNotifications(ids, partner_ids);
|
||||
this.pyEnv['bus.bus']._sendmany(notifications);
|
||||
},
|
||||
/**
|
||||
* Simulates `_channel_channel_notifications` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @param {integer[]} partner_ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailChannel_channelChannelNotifications(ids, partner_ids) {
|
||||
const notifications = [];
|
||||
for (const partner_id of partner_ids) {
|
||||
const user = this.getRecords('res.users', [['partner_id', 'in', partner_id]])[0];
|
||||
if (!user) {
|
||||
continue;
|
||||
}
|
||||
// Note: `channel_info` on the server is supposed to be called with
|
||||
// the proper user context but this is not done here for simplicity.
|
||||
const channelInfos = this._mockMailChannelChannelInfo(ids);
|
||||
const [relatedPartner] = this.pyEnv['res.partner'].searchRead([['id', '=', partner_id]]);
|
||||
for (const channelInfo of channelInfos) {
|
||||
notifications.push([relatedPartner, 'mail.channel/legacy_insert', channelInfo]);
|
||||
}
|
||||
}
|
||||
return notifications;
|
||||
},
|
||||
/**
|
||||
* Simulates `channel_fetched` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @param {string} extra_info
|
||||
*/
|
||||
_mockMailChannelChannelFetched(ids) {
|
||||
const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
|
||||
for (const channel of channels) {
|
||||
const channelMessages = this.getRecords('mail.message', [
|
||||
['model', '=', 'mail.channel'],
|
||||
['res_id', '=', channel.id],
|
||||
]);
|
||||
const lastMessage = channelMessages.reduce((lastMessage, message) => {
|
||||
if (message.id > lastMessage.id) {
|
||||
return message;
|
||||
}
|
||||
return lastMessage;
|
||||
}, channelMessages[0]);
|
||||
if (!lastMessage) {
|
||||
continue;
|
||||
}
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
|
||||
this.pyEnv['mail.channel.member'].write(
|
||||
[memberOfCurrentUser.id],
|
||||
{ fetched_message_id: lastMessage.id },
|
||||
);
|
||||
this.pyEnv['bus.bus']._sendone(channel, 'mail.channel.member/fetched', {
|
||||
'channel_id': channel.id,
|
||||
'id': memberOfCurrentUser.id,
|
||||
'last_message_id': lastMessage.id,
|
||||
'partner_id': this.currentPartnerId,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates `channel_fetch_preview` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object[]} list of channels previews
|
||||
*/
|
||||
_mockMailChannelChannelFetchPreview(ids) {
|
||||
const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
|
||||
return channels.map(channel => {
|
||||
const channelMessages = this.getRecords('mail.message', [
|
||||
['model', '=', 'mail.channel'],
|
||||
['res_id', '=', channel.id],
|
||||
]);
|
||||
const lastMessage = channelMessages.reduce((lastMessage, message) => {
|
||||
if (message.id > lastMessage.id) {
|
||||
return message;
|
||||
}
|
||||
return lastMessage;
|
||||
}, channelMessages[0]);
|
||||
return {
|
||||
id: channel.id,
|
||||
last_message: lastMessage ? this._mockMailMessageMessageFormat([lastMessage.id])[0] : false,
|
||||
};
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates the 'channel_fold' route on `mail.channel`.
|
||||
* In particular sends a notification on the bus.
|
||||
*
|
||||
* @private
|
||||
* @param {number} ids
|
||||
* @param {state} [state]
|
||||
*/
|
||||
_mockMailChannelChannelFold(ids, state) {
|
||||
const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
|
||||
for (const channel of channels) {
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
|
||||
const foldState = state ? state : memberOfCurrentUser.fold_state === 'open' ? 'folded' : 'open';
|
||||
const vals = {
|
||||
fold_state: foldState,
|
||||
is_minimized: foldState !== 'closed',
|
||||
};
|
||||
this.pyEnv['mail.channel.member'].write([memberOfCurrentUser.id], vals);
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.thread/insert', {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'serverFoldState': memberOfCurrentUser.fold_state,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates 'channel_get' on 'mail.channel'.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} [partners_to=[]]
|
||||
* @param {boolean} [pin=true]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailChannelChannelGet(partners_to = [], pin = true) {
|
||||
if (partners_to.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (!partners_to.includes(this.currentPartnerId)) {
|
||||
partners_to.push(this.currentPartnerId);
|
||||
}
|
||||
const partners = this.getRecords('res.partner', [['id', 'in', partners_to]]);
|
||||
// NOTE: this mock is not complete, which is done for simplicity.
|
||||
// Indeed if a chat already exists for the given partners, the server
|
||||
// is supposed to return this existing chat. But the mock is currently
|
||||
// always creating a new chat, because no test is relying on receiving
|
||||
// an existing chat.
|
||||
const id = this.pyEnv['mail.channel'].create({
|
||||
channel_member_ids: partners.map(partner => [0, 0, {
|
||||
partner_id: partner.id,
|
||||
}]),
|
||||
channel_type: 'chat',
|
||||
name: partners.map(partner => partner.name).join(", "),
|
||||
});
|
||||
return this._mockMailChannelChannelInfo([id])[0];
|
||||
},
|
||||
/**
|
||||
* Simulates `channel_info` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailChannelChannelInfo(ids) {
|
||||
const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
|
||||
return channels.map(channel => {
|
||||
const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
|
||||
const messages = this.getRecords('mail.message', [
|
||||
['model', '=', 'mail.channel'],
|
||||
['res_id', '=', channel.id],
|
||||
]);
|
||||
const [group_public_id] = this.getRecords('res.groups', [
|
||||
['id', '=', channel.group_public_id],
|
||||
]);
|
||||
const lastMessageId = messages.reduce((lastMessageId, message) => {
|
||||
if (!lastMessageId || message.id > lastMessageId) {
|
||||
return message.id;
|
||||
}
|
||||
return lastMessageId;
|
||||
}, undefined);
|
||||
const messageNeedactionCounter = this.getRecords('mail.notification', [
|
||||
['res_partner_id', '=', this.currentPartnerId],
|
||||
['is_read', '=', false],
|
||||
['mail_message_id', 'in', messages.map(message => message.id)],
|
||||
]).length;
|
||||
const channelData = {
|
||||
avatarCacheKey: channel.avatarCacheKey,
|
||||
channel_type: channel.channel_type,
|
||||
id: channel.id,
|
||||
memberCount: channel.member_count,
|
||||
};
|
||||
const res = Object.assign({}, channel, {
|
||||
last_message_id: lastMessageId,
|
||||
message_needaction_counter: messageNeedactionCounter,
|
||||
authorizedGroupFullName: group_public_id ? group_public_id.name : false,
|
||||
});
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]);
|
||||
if (memberOfCurrentUser) {
|
||||
Object.assign(res, {
|
||||
is_minimized: memberOfCurrentUser.is_minimized,
|
||||
is_pinned: memberOfCurrentUser.is_pinned,
|
||||
last_interest_dt: memberOfCurrentUser.last_interest_dt,
|
||||
message_unread_counter: memberOfCurrentUser.message_unread_counter,
|
||||
state: memberOfCurrentUser.fold_state || 'open',
|
||||
});
|
||||
Object.assign(channelData, {
|
||||
custom_channel_name: memberOfCurrentUser.custom_channel_name,
|
||||
serverMessageUnreadCounter: memberOfCurrentUser.message_unread_counter,
|
||||
});
|
||||
if (memberOfCurrentUser.rtc_inviting_session_id) {
|
||||
res['rtc_inviting_session'] = { 'id': memberOfCurrentUser.rtc_inviting_session_id };
|
||||
}
|
||||
channelData['channelMembers'] = [['insert', this._mockMailChannelMember_MailChannelMemberFormat([memberOfCurrentUser.id])]];
|
||||
}
|
||||
if (channel.channel_type !== 'channel') {
|
||||
res['seen_partners_info'] = members.filter(member => member.partner_id).map(member => {
|
||||
return {
|
||||
partner_id: member.partner_id,
|
||||
seen_message_id: member.seen_message_id,
|
||||
fetched_message_id: member.fetched_message_id,
|
||||
};
|
||||
});
|
||||
channelData['channelMembers'] = [['insert', this._mockMailChannelMember_MailChannelMemberFormat(members.map(member => member.id))]];
|
||||
}
|
||||
res.channel = channelData;
|
||||
return res;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates the `channel_pin` method of `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {number[]} ids
|
||||
* @param {boolean} [pinned=false]
|
||||
*/
|
||||
async _mockMailChannelChannelPin(ids, pinned = false) {
|
||||
const [channel] = this.getRecords('mail.channel', [['id', 'in', ids]]);
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId], ['is_pinned', '!=', pinned]]);
|
||||
if (memberOfCurrentUser) {
|
||||
this.pyEnv['mail.channel.member'].write(
|
||||
[memberOfCurrentUser.id],
|
||||
{ is_pinned: pinned },
|
||||
);
|
||||
}
|
||||
if (!pinned) {
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/unpin', {
|
||||
'id': channel.id,
|
||||
});
|
||||
} else {
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/legacy_insert', this._mockMailChannelChannelInfo([channel.id])[0]);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates the `_channel_seen` method of `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param integer[] ids
|
||||
* @param {integer} last_message_id
|
||||
*/
|
||||
async _mockMailChannel_ChannelSeen(ids, last_message_id) {
|
||||
// Update record
|
||||
const channel_id = ids[0];
|
||||
if (!channel_id) {
|
||||
throw new Error('Should only be one channel in channel_seen mock params');
|
||||
}
|
||||
const channel = this.getRecords('mail.channel', [['id', '=', channel_id]])[0];
|
||||
const messagesBeforeGivenLastMessage = this.getRecords('mail.message', [
|
||||
['id', '<=', last_message_id],
|
||||
['model', '=', 'mail.channel'],
|
||||
['res_id', '=', channel.id],
|
||||
]);
|
||||
if (!messagesBeforeGivenLastMessage || messagesBeforeGivenLastMessage.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', this.currentPartnerId]]);
|
||||
if (memberOfCurrentUser.seen_message_id && memberOfCurrentUser.seen_message_id >= last_message_id) {
|
||||
return;
|
||||
}
|
||||
this._mockMailChannel_SetLastSeenMessage([channel.id], last_message_id);
|
||||
this.pyEnv['bus.bus']._sendone(channel.channel_type === 'chat' ? channel : this.pyEnv.currentPartner, 'mail.channel.member/seen', {
|
||||
'channel_id': channel.id,
|
||||
'last_message_id': last_message_id,
|
||||
'partner_id': this.currentPartnerId,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates `channel_rename` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
*/
|
||||
_mockMailChannelChannelRename(ids, name) {
|
||||
const channel = this.getRecords('mail.channel', [['id', 'in', ids]])[0];
|
||||
this.pyEnv['mail.channel'].write(
|
||||
[channel.id],
|
||||
{ name },
|
||||
);
|
||||
this.pyEnv['bus.bus']._sendone(channel, 'mail.thread/insert', {
|
||||
'id': channel.id,
|
||||
'model': 'mail.channel',
|
||||
'name': name,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates `channel_set_custom_name` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
*/
|
||||
_mockMailChannelChannelSetCustomName(ids, name) {
|
||||
const channelId = ids[0]; // simulate ensure_one.
|
||||
const [memberIdOfCurrentUser] = this.pyEnv['mail.channel.member'].search([['partner_id', '=', this.currentPartnerId], ['channel_id', '=', channelId]]);
|
||||
this.pyEnv['mail.channel.member'].write(
|
||||
[memberIdOfCurrentUser],
|
||||
{ custom_channel_name: name },
|
||||
);
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/insert', {
|
||||
'custom_channel_name': name,
|
||||
'id': channelId,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates the `create_group` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} partners_to
|
||||
* @returns {Object}
|
||||
*/
|
||||
async _mockMailChannelCreateGroup(partners_to) {
|
||||
const partners = this.getRecords('res.partner', [['id', 'in', partners_to]]);
|
||||
const id = this.pyEnv['mail.channel'].create({
|
||||
channel_type: 'group',
|
||||
channel_member_ids: partners.map(partner => [0, 0, { partner_id: partner.id }]),
|
||||
name: '',
|
||||
});
|
||||
this._mockMailChannel_broadcast(id, partners.map(partner => partner.id));
|
||||
return this._mockMailChannelChannelInfo([id])[0];
|
||||
},
|
||||
/**
|
||||
* Simulates `execute_command_leave` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_mockMailChannelExecuteCommandLeave(args) {
|
||||
const channel = this.getRecords('mail.channel', [['id', 'in', args.args[0]]])[0];
|
||||
if (channel.channel_type === 'channel') {
|
||||
this._mockMailChannelActionUnfollow([channel.id]);
|
||||
} else {
|
||||
this._mockMailChannelChannelPin(channel.uuid, false);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates `execute_command_who` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_mockMailChannelExecuteCommandWho(args) {
|
||||
const ids = args.args[0];
|
||||
const channels = this.getRecords('mail.channel', [['id', 'in', ids]]);
|
||||
for (const channel of channels) {
|
||||
const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
|
||||
const otherPartnerIds = members.filter(member => member.partner_id && member.partner_id !== this.currentPartnerId).map(member => member.partner_id);
|
||||
const otherPartners = this.getRecords('res.partner', [['id', 'in', otherPartnerIds]]);
|
||||
let message = "You are alone in this channel.";
|
||||
if (otherPartners.length > 0) {
|
||||
message = `Users in this channel: ${otherPartners.map(partner => partner.name).join(', ')} and you`;
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/transient_message', {
|
||||
'body': `<span class="o_mail_notification">${message}</span>`,
|
||||
'model': 'mail.channel',
|
||||
'res_id': channel.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates `get_mention_suggestions` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array[]}
|
||||
*/
|
||||
_mockMailChannelGetMentionSuggestions(args) {
|
||||
const search = args.kwargs.search || '';
|
||||
const limit = args.kwargs.limit || 8;
|
||||
|
||||
/**
|
||||
* Returns the given list of channels after filtering it according to
|
||||
* the logic of the Python method `get_mention_suggestions` for the
|
||||
* given search term. The result is truncated to the given limit and
|
||||
* formatted as expected by the original method.
|
||||
*
|
||||
* @param {Object[]} channels
|
||||
* @param {string} search
|
||||
* @param {integer} limit
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
const mentionSuggestionsFilter = function (channels, search, limit) {
|
||||
const matchingChannels = channels
|
||||
.filter(channel => {
|
||||
// no search term is considered as return all
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
// otherwise name or email must match search term
|
||||
if (channel.name && channel.name.includes(search)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).map(channel => {
|
||||
// expected format
|
||||
return {
|
||||
authorizedGroupFullName: channel.group_public_id ? channel.group_public_id.name : false,
|
||||
channel: {
|
||||
channel_type: channel.channel_type,
|
||||
id: channel.id,
|
||||
},
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
};
|
||||
});
|
||||
// reduce results to max limit
|
||||
matchingChannels.length = Math.min(matchingChannels.length, limit);
|
||||
return matchingChannels;
|
||||
};
|
||||
|
||||
const mentionSuggestions = mentionSuggestionsFilter(this.models['mail.channel'].records, search, limit);
|
||||
|
||||
return mentionSuggestions;
|
||||
},
|
||||
/**
|
||||
* Simulates `write` on `mail.channel` when `image_128` changes.
|
||||
*
|
||||
* @param {integer} id
|
||||
*/
|
||||
_mockMailChannelWriteImage128(id) {
|
||||
this.pyEnv['mail.channel'].write(
|
||||
[id],
|
||||
{
|
||||
avatarCacheKey: moment.utc().format("YYYYMMDDHHmmss"),
|
||||
},
|
||||
);
|
||||
const channel = this.pyEnv['mail.channel'].searchRead([['id', '=', id]])[0];
|
||||
this.pyEnv['bus.bus']._sendone(channel, 'mail.channel/insert', {
|
||||
'avatarCacheKey': channel.avatarCacheKey,
|
||||
'id': id,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates `load_more_members` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} channel_ids
|
||||
* @param {integer[]} known_member_ids
|
||||
*/
|
||||
_mockMailChannelLoadMoreMembers(channel_ids, known_member_ids) {
|
||||
const members = this.pyEnv['mail.channel.member'].searchRead([
|
||||
['id', 'not in', known_member_ids],
|
||||
['channel_id', 'in', channel_ids],
|
||||
], { limit: 100 });
|
||||
const memberCount = this.pyEnv['mail.channel.member'].searchCount([
|
||||
['channel_id', 'in', channel_ids],
|
||||
]);
|
||||
const membersData = [];
|
||||
for (const member of members) {
|
||||
let persona;
|
||||
if (member.partner_id) {
|
||||
const [partner] = this.pyEnv['res.partner'].searchRead(
|
||||
[['id', '=', member.partner_id[0]]],
|
||||
{ fields: ['id', 'name', 'im_status'] }
|
||||
);
|
||||
persona = {
|
||||
'partner': {
|
||||
'id': partner.id,
|
||||
'name': partner.name,
|
||||
'im_status': partner.im_status,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (member.guest_id) {
|
||||
const [guest] = this.pyEnv['mail.guest'].searchRead(
|
||||
[['id', '=', member.guest_id[0]]],
|
||||
{ fields: ['id', 'name'] }
|
||||
);
|
||||
persona = {
|
||||
'guest': {
|
||||
'id': guest.id,
|
||||
'name': guest.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
membersData.push({
|
||||
'id': member.id,
|
||||
'persona': persona,
|
||||
});
|
||||
}
|
||||
return {
|
||||
channelMembers: [['insert', membersData]],
|
||||
memberCount,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates the `_set_last_seen_message` method of `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @param {integer} message_id
|
||||
*/
|
||||
_mockMailChannel_SetLastSeenMessage(ids, message_id) {
|
||||
const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', 'in', ids], ['partner_id', '=', this.currentPartnerId]]);
|
||||
this.pyEnv['mail.channel.member'].write([memberOfCurrentUser.id], {
|
||||
fetched_message_id: message_id,
|
||||
seen_message_id: message_id,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_link_preview', {
|
||||
/**
|
||||
* Simulates the `_link_preview_format` method of `mail.link.preview`.
|
||||
*
|
||||
* @private
|
||||
* @param {object} linkPreview
|
||||
*/
|
||||
_mockMailLinkPreviewFormat(linkPreview) {
|
||||
return {
|
||||
id: linkPreview.id,
|
||||
image_mimetype: linkPreview.image_mimetype,
|
||||
og_description: linkPreview.og_description,
|
||||
og_image: linkPreview.og_image,
|
||||
og_mimetype: linkPreview.og_mimetype,
|
||||
og_title: linkPreview.og_title,
|
||||
og_type: linkPreview.og_type,
|
||||
source_url: linkPreview.source_url
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_message', {
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'mail.message' && args.method === 'mark_all_as_read') {
|
||||
const domain = args.args[0] || args.kwargs.domain;
|
||||
return this._mockMailMessageMarkAllAsRead(domain);
|
||||
}
|
||||
if (args.model === 'mail.message' && args.method === 'message_format') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailMessageMessageFormat(ids);
|
||||
}
|
||||
if (args.model === 'mail.message' && args.method === 'set_message_done') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailMessageSetMessageDone(ids);
|
||||
}
|
||||
if (args.model === 'mail.message' && args.method === 'toggle_message_starred') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailMessageToggleMessageStarred(ids);
|
||||
}
|
||||
if (args.model === 'mail.message' && args.method === 'unstar_all') {
|
||||
return this._mockMailMessageUnstarAll();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates `mark_all_as_read` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @param {Array[]} [domain]
|
||||
* @returns {integer[]}
|
||||
*/
|
||||
_mockMailMessageMarkAllAsRead(domain) {
|
||||
const notifDomain = [
|
||||
['res_partner_id', '=', this.currentPartnerId],
|
||||
['is_read', '=', false],
|
||||
];
|
||||
if (domain) {
|
||||
const messages = this.getRecords('mail.message', domain);
|
||||
const ids = messages.map(messages => messages.id);
|
||||
this._mockMailMessageSetMessageDone(ids);
|
||||
return ids;
|
||||
}
|
||||
const notifications = this.getRecords('mail.notification', notifDomain);
|
||||
this.pyEnv['mail.notification'].write(
|
||||
notifications.map(notification => notification.id),
|
||||
{ is_read: true },
|
||||
);
|
||||
const messageIds = [];
|
||||
for (const notification of notifications) {
|
||||
if (!messageIds.includes(notification.mail_message_id)) {
|
||||
messageIds.push(notification.mail_message_id);
|
||||
}
|
||||
}
|
||||
const messages = this.getRecords('mail.message', [['id', 'in', messageIds]]);
|
||||
// simulate compute that should be done based on notifications
|
||||
for (const message of messages) {
|
||||
this.pyEnv['mail.message'].write(
|
||||
[message.id],
|
||||
{
|
||||
needaction: false,
|
||||
needaction_partner_ids: message.needaction_partner_ids.filter(
|
||||
partnerId => partnerId !== this.currentPartnerId
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/mark_as_read', {
|
||||
'message_ids': messageIds,
|
||||
'needaction_inbox_counter': this._mockResPartner_GetNeedactionCount(this.currentPartnerId),
|
||||
});
|
||||
return messageIds;
|
||||
},
|
||||
/**
|
||||
* Simulates `_message_fetch` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @param {Array[]} domain
|
||||
* @param {integer} [max_id]
|
||||
* @param {integer} [min_id]
|
||||
* @param {integer} [limit=30]
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailMessage_MessageFetch(domain, max_id, min_id, limit = 30) {
|
||||
if (max_id) {
|
||||
domain.push(['id', '<', max_id]);
|
||||
}
|
||||
if (min_id) {
|
||||
domain.push(['id', '>', min_id]);
|
||||
}
|
||||
let messages = this.getRecords('mail.message', domain);
|
||||
// sorted from highest ID to lowest ID (i.e. from youngest to oldest)
|
||||
messages.sort(function (m1, m2) {
|
||||
return m1.id < m2.id ? 1 : -1;
|
||||
});
|
||||
// pick at most 'limit' messages
|
||||
messages.length = Math.min(messages.length, limit);
|
||||
return messages;
|
||||
},
|
||||
/**
|
||||
* Simulates `message_format` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailMessageMessageFormat(ids) {
|
||||
const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
|
||||
// sorted from highest ID to lowest ID (i.e. from most to least recent)
|
||||
messages.sort(function (m1, m2) {
|
||||
return m1.id < m2.id ? 1 : -1;
|
||||
});
|
||||
return messages.map(message => {
|
||||
const thread = message.model && this.getRecords(message.model, [
|
||||
['id', '=', message.res_id],
|
||||
])[0];
|
||||
let formattedAuthor;
|
||||
if (message.author_id) {
|
||||
const [author] = this.getRecords('res.partner', [['id', '=', message.author_id]], { active_test: false });
|
||||
formattedAuthor = {
|
||||
'id': author.id,
|
||||
'name': author.name,
|
||||
};
|
||||
} else {
|
||||
formattedAuthor = [['clear']];
|
||||
}
|
||||
const attachments = this.getRecords('ir.attachment', [
|
||||
['id', 'in', message.attachment_ids],
|
||||
]);
|
||||
const formattedAttachments = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id));
|
||||
const allNotifications = this.getRecords('mail.notification', [
|
||||
['mail_message_id', '=', message.id],
|
||||
]);
|
||||
const historyPartnerIds = allNotifications
|
||||
.filter(notification => notification.is_read)
|
||||
.map(notification => notification.res_partner_id);
|
||||
const needactionPartnerIds = allNotifications
|
||||
.filter(notification => !notification.is_read)
|
||||
.map(notification => notification.res_partner_id);
|
||||
let notifications = this._mockMailNotification_FilteredForWebClient(
|
||||
allNotifications.map(notification => notification.id)
|
||||
);
|
||||
notifications = this._mockMailNotification_NotificationFormat(
|
||||
notifications.map(notification => notification.id)
|
||||
);
|
||||
const trackingValueIds = this.getRecords('mail.tracking.value', [
|
||||
['id', 'in', message.tracking_value_ids],
|
||||
]);
|
||||
const formattedTrackingValues = this._mockMailTrackingValue_TrackingValueFormat(trackingValueIds);
|
||||
const partners = this.getRecords(
|
||||
'res.partner',
|
||||
[['id', 'in', message.partner_ids]],
|
||||
);
|
||||
const linkPreviews = this.getRecords('mail.link.preview', [
|
||||
['id', 'in', message.link_preview_ids],
|
||||
]);
|
||||
const linkPreviewsFormatted = linkPreviews.map(linkPreview => this._mockMailLinkPreviewFormat(linkPreview));
|
||||
|
||||
const response = Object.assign({}, message, {
|
||||
attachment_ids: formattedAttachments,
|
||||
author: formattedAuthor,
|
||||
history_partner_ids: historyPartnerIds,
|
||||
linkPreviews: linkPreviewsFormatted,
|
||||
needaction_partner_ids: needactionPartnerIds,
|
||||
notifications,
|
||||
parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false,
|
||||
recipients: partners.map(p => ({ id: p.id, name: p.name })),
|
||||
record_name: thread && (thread.name !== undefined ? thread.name : thread.display_name),
|
||||
trackingValues: formattedTrackingValues,
|
||||
});
|
||||
delete response['author_id'];
|
||||
if (message.subtype_id) {
|
||||
const subtype = this.getRecords('mail.message.subtype', [
|
||||
['id', '=', message.subtype_id],
|
||||
])[0];
|
||||
response.subtype_description = subtype.description;
|
||||
}
|
||||
if (message.author_guest_id) {
|
||||
const [guest] = this.pyEnv['mail.guest'].searchRead([['id', '=', message.author_guest_id]]);
|
||||
response['guestAuthor'] = { id: guest.id, name: guest.name };
|
||||
}
|
||||
return response;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates `_message_notification_format` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailMessage_MessageNotificationFormat(ids) {
|
||||
const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
|
||||
return messages.map(message => {
|
||||
let notifications = this.getRecords('mail.notification', [
|
||||
['mail_message_id', '=', message.id],
|
||||
]);
|
||||
notifications = this._mockMailNotification_FilteredForWebClient(
|
||||
notifications.map(notification => notification.id)
|
||||
);
|
||||
notifications = this._mockMailNotification_NotificationFormat(
|
||||
notifications.map(notification => notification.id)
|
||||
);
|
||||
return {
|
||||
'date': message.date,
|
||||
'id': message.id,
|
||||
'message_type': message.message_type,
|
||||
'model': message.model,
|
||||
'notifications': notifications,
|
||||
'res_id': message.res_id,
|
||||
'res_model_name': message.res_model_name,
|
||||
};
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates `set_message_done` on `mail.message`, which turns provided
|
||||
* needaction message to non-needaction (i.e. they are marked as read from
|
||||
* from the Inbox mailbox). Also notify on the longpoll bus that the
|
||||
* messages have been marked as read, so that UI is updated.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
*/
|
||||
_mockMailMessageSetMessageDone(ids) {
|
||||
const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
|
||||
|
||||
const notifications = this.getRecords('mail.notification', [
|
||||
['res_partner_id', '=', this.currentPartnerId],
|
||||
['is_read', '=', false],
|
||||
['mail_message_id', 'in', messages.map(messages => messages.id)]
|
||||
]);
|
||||
this.pyEnv['mail.notification'].write(
|
||||
notifications.map(notification => notification.id),
|
||||
{ is_read: true },
|
||||
);
|
||||
// simulate compute that should be done based on notifications
|
||||
for (const message of messages) {
|
||||
this.pyEnv['mail.message'].write(
|
||||
[message.id],
|
||||
{
|
||||
needaction: false,
|
||||
needaction_partner_ids: message.needaction_partner_ids.filter(
|
||||
partnerId => partnerId !== this.currentPartnerId
|
||||
),
|
||||
},
|
||||
);
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/mark_as_read', {
|
||||
'message_ids': [message.id],
|
||||
'needaction_inbox_counter': this._mockResPartner_GetNeedactionCount(this.currentPartnerId),
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates `toggle_message_starred` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @returns {integer[]} ids
|
||||
*/
|
||||
_mockMailMessageToggleMessageStarred(ids) {
|
||||
const messages = this.getRecords('mail.message', [['id', 'in', ids]]);
|
||||
for (const message of messages) {
|
||||
const wasStared = message.starred_partner_ids.includes(this.currentPartnerId);
|
||||
this.pyEnv['mail.message'].write(
|
||||
[message.id],
|
||||
{ starred_partner_ids: [[wasStared ? 3 : 4, this.currentPartnerId]] }
|
||||
);
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/toggle_star', {
|
||||
'message_ids': [message.id],
|
||||
'starred': !wasStared,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates `unstar_all` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_mockMailMessageUnstarAll() {
|
||||
const messages = this.getRecords('mail.message', [
|
||||
['starred_partner_ids', 'in', this.currentPartnerId],
|
||||
]);
|
||||
this.pyEnv['mail.message'].write(
|
||||
messages.map(message => message.id),
|
||||
{ starred_partner_ids: [[3, this.currentPartnerId]] }
|
||||
);
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/toggle_star', {
|
||||
'message_ids': messages.map(message => message.id),
|
||||
'starred': false,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_notification', {
|
||||
/**
|
||||
* Simulates `_filtered_for_web_client` on `mail.notification`.
|
||||
*
|
||||
* @private
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailNotification_FilteredForWebClient(ids) {
|
||||
const notifications = this.getRecords('mail.notification', [
|
||||
['id', 'in', ids],
|
||||
]);
|
||||
return notifications.filter(notification => {
|
||||
const partner = this.getRecords('res.partner', [['id', '=', notification.res_partner_id]])[0];
|
||||
if (['bounce', 'exception', 'canceled'].includes(notification.notification_status) ||
|
||||
(partner && partner.partner_share)) {
|
||||
return true;
|
||||
}
|
||||
const message = this.getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0];
|
||||
const subtypes = (message.subtype_id) ?
|
||||
this.getRecords('mail.message.subtype', [['id', '=', message.subtype_id]]) : [];
|
||||
return (subtypes.length == 0) || subtypes[0].track_recipients;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates `_notification_format` on `mail.notification`.
|
||||
*
|
||||
* @private
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailNotification_NotificationFormat(ids) {
|
||||
const notifications = this.getRecords('mail.notification', [['id', 'in', ids]]);
|
||||
return notifications.map(notification => {
|
||||
const partner = this.getRecords('res.partner', [['id', '=', notification.res_partner_id]])[0];
|
||||
return {
|
||||
'id': notification.id,
|
||||
'notification_type': notification.notification_type,
|
||||
'notification_status': notification.notification_status,
|
||||
'failure_type': notification.failure_type,
|
||||
'res_partner_id': partner ? [partner && partner.id, partner && partner.display_name] : undefined,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
import { datetime_to_str } from 'web.time';
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_thread', {
|
||||
async _performRPC(route, args) {
|
||||
if (args.method === 'message_subscribe') {
|
||||
const ids = args.args[0];
|
||||
const partner_ids = args.args[1] || args.kwargs.partner_ids;
|
||||
const subtype_ids = args.args[2] || args.kwargs.subtype_ids;
|
||||
return this._mockMailThreadMessageSubscribe(args.model, ids, partner_ids, subtype_ids);
|
||||
}
|
||||
if (args.method === 'message_unsubscribe') {
|
||||
const ids = args.args[0];
|
||||
const partner_ids = args.args[1] || args.kwargs.partner_ids;
|
||||
return this._mockMailThreadMessageUnsubscribe(args.model, ids, partner_ids);
|
||||
}
|
||||
if (args.method === 'message_post') {
|
||||
const id = args.args[0];
|
||||
const kwargs = args.kwargs;
|
||||
const context = kwargs.context;
|
||||
delete kwargs.context;
|
||||
return this._mockMailThreadMessagePost(args.model, [id], kwargs, context);
|
||||
}
|
||||
if (args.method === 'notify_cancel_by_type') {
|
||||
return this._mockMailThreadNotifyCancelByType(args.model, args.kwargs.notification_type);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates `_message_compute_author` on `mail.thread`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @param {Object} [context={}]
|
||||
* @returns {Array}
|
||||
*/
|
||||
_MockMailThread_MessageComputeAuthor(model, ids, author_id, email_from, context = {}) {
|
||||
if (author_id === undefined) {
|
||||
// For simplicity partner is not guessed from email_from here, but
|
||||
// that would be the first step on the server.
|
||||
let user_id;
|
||||
if ('mockedUserId' in context) {
|
||||
// can be falsy to simulate not being logged in
|
||||
user_id = context.mockedUserId
|
||||
? context.mockedUserId
|
||||
: this.publicUserId;
|
||||
} else {
|
||||
user_id = this.currentUserId;
|
||||
}
|
||||
const user = this.getRecords(
|
||||
'res.users',
|
||||
[['id', '=', user_id]],
|
||||
{ active_test: false },
|
||||
)[0];
|
||||
const author = this.getRecords(
|
||||
'res.partner',
|
||||
[['id', '=', user.partner_id]],
|
||||
{ active_test: false },
|
||||
)[0];
|
||||
author_id = author.id;
|
||||
email_from = `${author.display_name} <${author.email}>`;
|
||||
}
|
||||
|
||||
if (email_from === undefined) {
|
||||
if (author_id) {
|
||||
const author = this.getRecords(
|
||||
'res.partner',
|
||||
[['id', '=', author_id]],
|
||||
{ active_test: false },
|
||||
)[0];
|
||||
email_from = `${author.display_name} <${author.email}>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!email_from) {
|
||||
throw Error("Unable to log message due to missing author email.");
|
||||
}
|
||||
|
||||
return [author_id, email_from];
|
||||
},
|
||||
/**
|
||||
* Simulates `_message_add_suggested_recipient` on `mail.thread`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @param {Object} result
|
||||
* @param {Object} [param3={}]
|
||||
* @param {string} [param3.email]
|
||||
* @param {integer} [param3.partner]
|
||||
* @param {string} [param3.reason]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailThread_MessageAddSuggestedRecipient(model, ids, result, { email, partner, reason = '' } = {}) {
|
||||
const record = this.getRecords(model, [['id', 'in', 'ids']])[0];
|
||||
// for simplicity
|
||||
result[record.id].push([partner, email, reason]);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Simulates `_message_get_suggested_recipients` on `mail.thread`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailThread_MessageGetSuggestedRecipients(model, ids) {
|
||||
if (model === 'res.fake') {
|
||||
return this._mockResFake_MessageGetSuggestedRecipients(model, ids);
|
||||
}
|
||||
const result = ids.reduce((result, id) => result[id] = [], {});
|
||||
const records = this.getRecords(model, [['id', 'in', ids]]);
|
||||
for (const record in records) {
|
||||
if (record.user_id) {
|
||||
const user = this.getRecords('res.users', [['id', '=', record.user_id]]);
|
||||
if (user.partner_id) {
|
||||
const reason = this.models[model].fields['user_id'].string;
|
||||
this._mockMailThread_MessageAddSuggestedRecipient(result, user.partner_id, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Simulates `message_post` on `mail.thread`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @param {Object} kwargs
|
||||
* @param {Object} [context]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailThreadMessagePost(model, ids, kwargs, context) {
|
||||
const id = ids[0]; // ensure_one
|
||||
if (kwargs.attachment_ids) {
|
||||
const attachments = this.getRecords('ir.attachment', [
|
||||
['id', 'in', kwargs.attachment_ids],
|
||||
['res_model', '=', 'mail.compose.message'],
|
||||
['res_id', '=', 0],
|
||||
]);
|
||||
const attachmentIds = attachments.map(attachment => attachment.id);
|
||||
this.pyEnv['ir.attachment'].write(
|
||||
attachmentIds,
|
||||
{
|
||||
res_id: id,
|
||||
res_model: model,
|
||||
},
|
||||
);
|
||||
kwargs.attachment_ids = attachmentIds.map(attachmentId => [4, attachmentId]);
|
||||
}
|
||||
const subtype_xmlid = kwargs.subtype_xmlid || 'mail.mt_note';
|
||||
const [author_id, email_from] = this._MockMailThread_MessageComputeAuthor(
|
||||
model,
|
||||
ids,
|
||||
kwargs.author_id,
|
||||
kwargs.email_from, context,
|
||||
);
|
||||
const values = Object.assign({}, kwargs, {
|
||||
author_id,
|
||||
email_from,
|
||||
is_discussion: subtype_xmlid === 'mail.mt_comment',
|
||||
is_note: subtype_xmlid === 'mail.mt_note',
|
||||
model,
|
||||
res_id: id,
|
||||
});
|
||||
delete values.subtype_xmlid;
|
||||
const messageId = this.pyEnv['mail.message'].create(values);
|
||||
this._mockMailThread_NotifyThread(model, ids, messageId);
|
||||
return this._mockMailMessageMessageFormat([messageId])[0];
|
||||
},
|
||||
/**
|
||||
* Simulates `message_subscribe` on `mail.thread`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model not in server method but necessary for thread mock
|
||||
* @param {integer[]} ids
|
||||
* @param {integer[]} partner_ids
|
||||
* @param {integer[]} subtype_ids
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_mockMailThreadMessageSubscribe(model, ids, partner_ids, subtype_ids) {
|
||||
for (const id of ids) {
|
||||
for (const partner_id of partner_ids) {
|
||||
const followerId = this.pyEnv['mail.followers'].create({
|
||||
is_active: true,
|
||||
partner_id,
|
||||
res_id: id,
|
||||
res_model: model,
|
||||
subtype_ids: subtype_ids,
|
||||
});
|
||||
this.pyEnv['res.partner'].write([partner_id], {
|
||||
message_follower_ids: [followerId],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates `_notify_thread` on `mail.thread`.
|
||||
* Simplified version that sends notification to author and channel.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model not in server method but necessary for thread mock
|
||||
* @param {integer[]} ids
|
||||
* @param {integer} messageId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_mockMailThread_NotifyThread(model, ids, messageId) {
|
||||
const message = this.getRecords('mail.message', [['id', '=', messageId]])[0];
|
||||
const messageFormat = this._mockMailMessageMessageFormat([messageId])[0];
|
||||
const notifications = [];
|
||||
if (model === 'mail.channel') {
|
||||
// members
|
||||
const channels = this.getRecords('mail.channel', [['id', '=', message.res_id]]);
|
||||
for (const channel of channels) {
|
||||
notifications.push([channel, 'mail.channel/new_message', {
|
||||
'id': channel.id,
|
||||
'message': messageFormat,
|
||||
}]);
|
||||
// notify update of last_interest_dt
|
||||
const now = datetime_to_str(new Date());
|
||||
const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]);
|
||||
this.pyEnv['mail.channel.member'].write(
|
||||
members.map(member => member.id),
|
||||
{ last_interest_dt: now },
|
||||
);
|
||||
for (const member of members) {
|
||||
// simplification, send everything on the current user "test" bus, but it should send to each member instead
|
||||
notifications.push([member, 'mail.channel/last_interest_dt_changed', {
|
||||
'id': channel.id,
|
||||
'last_interest_dt': member.last_interest_dt,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendmany(notifications);
|
||||
},
|
||||
/**
|
||||
* Simulates `message_unsubscribe` on `mail.thread`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model not in server method but necessary for thread mock
|
||||
* @param {integer[]} ids
|
||||
* @param {integer[]} partner_ids
|
||||
* @returns {boolean|undefined}
|
||||
*/
|
||||
_mockMailThreadMessageUnsubscribe(model, ids, partner_ids) {
|
||||
if (!partner_ids) {
|
||||
return true;
|
||||
}
|
||||
const followers = this.getRecords('mail.followers', [
|
||||
['res_model', '=', model],
|
||||
['res_id', 'in', ids],
|
||||
['partner_id', 'in', partner_ids || []],
|
||||
]);
|
||||
this.pyEnv['mail.followers'].unlink(followers.map(follower => follower.id));
|
||||
},
|
||||
/**
|
||||
* Simulates `_message_track` on `mail.thread`
|
||||
*/
|
||||
_mockMailThread_MessageTrack(modelName, trackedFieldNames, initialTrackedFieldValuesByRecordId) {
|
||||
const trackFieldNamesToField = this.mockFieldsGet(modelName, trackedFieldNames);
|
||||
const tracking = {};
|
||||
const records = this.models[modelName].records;
|
||||
for (const record of records) {
|
||||
tracking[record.id] = this._mockMailBaseModel__MailTrack(modelName, trackFieldNamesToField, initialTrackedFieldValuesByRecordId[record.id], record);
|
||||
}
|
||||
for (const record of records) {
|
||||
const { trackingValueIds, changedFieldNames } = tracking[record.id] || {};
|
||||
if (!changedFieldNames || !changedFieldNames.length) {
|
||||
continue;
|
||||
}
|
||||
const changedFieldsInitialValues = {};
|
||||
const initialFieldValues = initialTrackedFieldValuesByRecordId[record.id];
|
||||
for (const fname in changedFieldNames) {
|
||||
changedFieldsInitialValues[fname] = initialFieldValues[fname];
|
||||
}
|
||||
const subtype = this._mockMailThread_TrackSubtype(changedFieldsInitialValues);
|
||||
this._mockMailThreadMessagePost(modelName, [record.id], {
|
||||
subtype_id: subtype.id,
|
||||
tracking_value_ids: trackingValueIds,
|
||||
});
|
||||
}
|
||||
return tracking;
|
||||
},
|
||||
/**
|
||||
* Simulates `_track_finalize` on `mail.thread`
|
||||
*/
|
||||
_mockMailThread_TrackFinalize(model, initialTrackedFieldValuesByRecordId) {
|
||||
this._mockMailThread_MessageTrack(
|
||||
model,
|
||||
this._mockMailThread_TrackGetFields(model),
|
||||
initialTrackedFieldValuesByRecordId,
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Simulates `_track_get_fields` on `mail.thread`
|
||||
*/
|
||||
_mockMailThread_TrackGetFields(model) {
|
||||
return Object.entries(this.models[model].fields).reduce((prev, next) => {
|
||||
if (next[1].tracking) {
|
||||
prev.push(next[0]);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
},
|
||||
/**
|
||||
* Simulates `_track_prepare` on `mail.thread`
|
||||
*/
|
||||
_mockMailThread_TrackPrepare(model) {
|
||||
const trackedFieldNames = this._mockMailThread_TrackGetFields(model);
|
||||
if (!trackedFieldNames.length) {
|
||||
return;
|
||||
}
|
||||
const initialTrackedFieldValuesByRecordId = {};
|
||||
for (const record of this.models[model].records) {
|
||||
const values = {};
|
||||
initialTrackedFieldValuesByRecordId[record.id] = values;
|
||||
for (const fname of trackedFieldNames) {
|
||||
values[fname] = record[fname];
|
||||
}
|
||||
}
|
||||
return initialTrackedFieldValuesByRecordId;
|
||||
},
|
||||
/**
|
||||
* Simulates `_track_subtype` on `mail.thread`
|
||||
*/
|
||||
_mockMailThread_TrackSubtype(initialFieldValuesByRecordId) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Simulate the `notify_cancel_by_type` on `mail.thread` .
|
||||
* Note that this method is overridden by snailmail module but not simulated here.
|
||||
*/
|
||||
_mockMailThreadNotifyCancelByType(model, notificationType) {
|
||||
// Query matching notifications
|
||||
const notifications = this.getRecords('mail.notification', [
|
||||
['notification_type', '=', notificationType],
|
||||
['notification_status', 'in', ['bounce', 'exception']],
|
||||
]).filter(notification => {
|
||||
const message = this.getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0];
|
||||
return message.model === model && message.author_id === this.currentPartnerId;
|
||||
});
|
||||
// Update notification status
|
||||
this.pyEnv['mail.notification'].write(
|
||||
notifications.map(notification => notification.id),
|
||||
{ notification_status: 'canceled' },
|
||||
);
|
||||
// Send bus notifications to update status of notifications in the web client
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/notification_update', {
|
||||
'elements': this._mockMailMessage_MessageNotificationFormat(
|
||||
notifications.map(notification => notification.mail_message_id)
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/mail_tracking_value', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init(data, options) {
|
||||
this._super(data, options);
|
||||
if (this.currentPartnerId && this.models && 'res.partner' in this.models) {
|
||||
this.currentPartner = this.getRecords('res.partner', [['id', '=', this.currentPartnerId]])[0];
|
||||
}
|
||||
// creation of the ir.model.fields records, required for tracked fields
|
||||
for (const modelName in this.models) {
|
||||
const fieldNamesToFields = this.models[modelName].fields;
|
||||
for (const fname in fieldNamesToFields) {
|
||||
if (fieldNamesToFields[fname].tracking) {
|
||||
this.mockCreate('ir.model.fields', { model: modelName, name: fname });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
mockWrite(model) {
|
||||
const initialTrackedFieldValuesByRecordId = this._mockMailThread_TrackPrepare(model);
|
||||
const mockWriteResult = this._super(...arguments);
|
||||
if (initialTrackedFieldValuesByRecordId) {
|
||||
this._mockMailThread_TrackFinalize(model, initialTrackedFieldValuesByRecordId);
|
||||
}
|
||||
return mockWriteResult;
|
||||
},
|
||||
/**
|
||||
* Simulates `create_tracking_values` on `mail.tracking.value`
|
||||
*/
|
||||
_mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fieldName, field, modelName) {
|
||||
let isTracked = true;
|
||||
const irField = this.models['ir.model.fields'].records.find(field => field.model === modelName && field.name === fieldName);
|
||||
|
||||
if (!irField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = { field: irField['id'], field_desc: field['string'], field_type: field['type'] };
|
||||
switch (values.field_type) {
|
||||
case 'char':
|
||||
case 'datetime':
|
||||
case 'float':
|
||||
case 'integer':
|
||||
case 'monetary':
|
||||
case 'text':
|
||||
values[`old_value_${values.field_type}`] = initialValue;
|
||||
values[`new_value_${values.field_type}`] = newValue;
|
||||
break;
|
||||
case 'date':
|
||||
values['old_value_datetime'] = initialValue;
|
||||
values['new_value_datetime'] = newValue;
|
||||
break;
|
||||
case 'boolean':
|
||||
values['old_value_integer'] = initialValue ? 1 : 0;
|
||||
values['new_value_integer'] = newValue ? 1 : 0;
|
||||
break;
|
||||
case 'selection':
|
||||
values['old_value_char'] = initialValue;
|
||||
values['new_value_char'] = newValue;
|
||||
break;
|
||||
case 'many2one':
|
||||
initialValue = initialValue ? this.pyEnv[field.relation].searchRead([['id', '=', initialValue]])[0] : initialValue;
|
||||
newValue = newValue ? this.pyEnv[field.relation].searchRead([['id', '=', newValue]])[0] : newValue;
|
||||
values['old_value_integer'] = initialValue ? initialValue.id : 0;
|
||||
values['new_value_integer'] = newValue ? newValue.id : 0;
|
||||
values['old_value_char'] = initialValue ? initialValue.display_name : '';
|
||||
values['new_value_char'] = newValue ? newValue.display_name : '';
|
||||
break;
|
||||
default:
|
||||
isTracked = false;
|
||||
}
|
||||
if (isTracked) {
|
||||
return this.pyEnv['mail.tracking.value'].create(values);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Simulates `_tracking_value_format` on `mail.tracking.value`
|
||||
*/
|
||||
_mockMailTrackingValue_TrackingValueFormat(tracking_value_ids) {
|
||||
const trackingValues = tracking_value_ids.map(tracking => ({
|
||||
changedField: tracking.field_desc,
|
||||
id: tracking.id,
|
||||
newValue: {
|
||||
fieldType: tracking.field_type,
|
||||
value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'new')
|
||||
},
|
||||
oldValue: {
|
||||
fieldType: tracking.field_type,
|
||||
value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'old')
|
||||
},
|
||||
}));
|
||||
return trackingValues;
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_display_value` on `mail.tracking.value`
|
||||
*/
|
||||
_mockMailTrackingValue_GetDisplayValue(record, type) {
|
||||
switch (record.field_type) {
|
||||
case 'float':
|
||||
case 'integer':
|
||||
case 'monetary':
|
||||
case 'text':
|
||||
return record[`${type}_value_${record.field_type}`];
|
||||
case 'datetime':
|
||||
if (record[`${type}_value_datetime`]) {
|
||||
const datetime = record[`${type}_value_datetime`];
|
||||
return `${datetime}Z`;
|
||||
} else {
|
||||
return record[`${type}_value_datetime`];
|
||||
}
|
||||
case 'date':
|
||||
if (record[`${type}_value_datetime`]) {
|
||||
return record[`${type}_value_datetime`];
|
||||
} else {
|
||||
return record[`${type}_value_datetime`];
|
||||
}
|
||||
case 'boolean':
|
||||
return !!record[`${type}_value_integer`];
|
||||
default:
|
||||
return record[`${type}_value_char`];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/models', {
|
||||
/**
|
||||
* Simulates `_mail_track` on `base`
|
||||
*
|
||||
* @param {string} model
|
||||
* @param {Object} trackedFieldNamesToField
|
||||
* @param {Object} initialTrackedFieldValues
|
||||
* @param {Object} record
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailBaseModel__MailTrack(model, trackedFieldNamesToField, initialTrackedFieldValues, record) {
|
||||
const trackingValueIds = [];
|
||||
const changedFieldNames = [];
|
||||
for (const fname in trackedFieldNamesToField) {
|
||||
const initialValue = initialTrackedFieldValues[fname];
|
||||
const newValue = record[fname];
|
||||
if (initialValue !== newValue) {
|
||||
const tracking = this._mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fname, trackedFieldNamesToField[fname], model);
|
||||
if (tracking) {
|
||||
trackingValueIds.push(tracking);
|
||||
}
|
||||
changedFieldNames.push(fname);
|
||||
}
|
||||
}
|
||||
return { changedFieldNames, trackingValueIds };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/res_fake', {
|
||||
/**
|
||||
* Simulates `_message_get_suggested_recipients` on `res.fake`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockResFake_MessageGetSuggestedRecipients(model, ids) {
|
||||
const result = {};
|
||||
const records = this.getRecords(model, [['id', 'in', ids]]);
|
||||
|
||||
for (const record of records) {
|
||||
result[record.id] = [];
|
||||
if (record.email_cc) {
|
||||
result[record.id].push([
|
||||
false,
|
||||
record.email_cc,
|
||||
undefined,
|
||||
'CC email',
|
||||
]);
|
||||
}
|
||||
const partners = this.getRecords(
|
||||
'res.partner',
|
||||
[['id', 'in', record.partner_ids]],
|
||||
);
|
||||
if (partners.length) {
|
||||
for (const partner of partners) {
|
||||
result[record.id].push([
|
||||
partner.id,
|
||||
partner.display_name,
|
||||
undefined,
|
||||
'Email partner',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/res_partner', {
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'res.partner' && args.method === 'im_search') {
|
||||
const name = args.args[0] || args.kwargs.search;
|
||||
const limit = args.args[1] || args.kwargs.limit;
|
||||
return this._mockResPartnerImSearch(name, limit);
|
||||
}
|
||||
if (args.model === 'res.partner' && args.method === 'search_for_channel_invite') {
|
||||
const search_term = args.args[0] || args.kwargs.search_term;
|
||||
const channel_id = args.args[1] || args.kwargs.channel_id;
|
||||
const limit = args.args[2] || args.kwargs.limit;
|
||||
return this._mockResPartnerSearchForChannelInvite(search_term, channel_id, limit);
|
||||
}
|
||||
if (args.model === 'res.partner' && args.method === 'get_mention_suggestions') {
|
||||
return this._mockResPartnerGetMentionSuggestions(args);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates `get_mention_suggestions` on `res.partner`.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array[]}
|
||||
*/
|
||||
_mockResPartnerGetMentionSuggestions(args) {
|
||||
const search = (args.args[0] || args.kwargs.search || '').toLowerCase();
|
||||
const limit = args.args[1] || args.kwargs.limit || 8;
|
||||
const channel_id = args.args[2] || args.kwargs.channel_id;
|
||||
|
||||
/**
|
||||
* Returns the given list of partners after filtering it according to
|
||||
* the logic of the Python method `get_mention_suggestions` for the
|
||||
* given search term. The result is truncated to the given limit and
|
||||
* formatted as expected by the original method.
|
||||
*
|
||||
* @param {Object[]} partners
|
||||
* @param {string} search
|
||||
* @param {integer} limit
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
const mentionSuggestionsFilter = (partners, search, limit) => {
|
||||
const matchingPartners = [...this._mockResPartnerMailPartnerFormat(
|
||||
partners
|
||||
.filter(partner => {
|
||||
if (channel_id) {
|
||||
const [member] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partner.id]]);
|
||||
if (!member) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// no search term is considered as return all
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
// otherwise name or email must match search term
|
||||
if (partner.name && partner.name.toLowerCase().includes(search)) {
|
||||
return true;
|
||||
}
|
||||
if (partner.email && partner.email.toLowerCase().includes(search)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(partner => partner.id)
|
||||
).values()].map(partnerFormat => {
|
||||
if (channel_id) {
|
||||
const [member] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerFormat.id]]);
|
||||
partnerFormat['persona'] = {
|
||||
'channelMembers': [['insert', this._mockMailChannelMember_MailChannelMemberFormat([member.id])]],
|
||||
};
|
||||
}
|
||||
return partnerFormat;
|
||||
});
|
||||
// reduce results to max limit
|
||||
matchingPartners.length = Math.min(matchingPartners.length, limit);
|
||||
return matchingPartners;
|
||||
};
|
||||
|
||||
// add main suggestions based on users
|
||||
const partnersFromUsers = this.getRecords('res.users', [])
|
||||
.map(user => this.getRecords('res.partner', [['id', '=', user.partner_id]])[0])
|
||||
.filter(partner => partner);
|
||||
const mainMatchingPartners = mentionSuggestionsFilter(partnersFromUsers, search, limit);
|
||||
|
||||
let extraMatchingPartners = [];
|
||||
// if not enough results add extra suggestions based on partners
|
||||
const remainingLimit = limit - mainMatchingPartners.length;
|
||||
if (mainMatchingPartners.length < limit) {
|
||||
const partners = this.getRecords('res.partner', [['id', 'not in', mainMatchingPartners.map(partner => partner.id)]]);
|
||||
extraMatchingPartners = mentionSuggestionsFilter(partners, search, remainingLimit);
|
||||
}
|
||||
return mainMatchingPartners.concat(extraMatchingPartners);
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_needaction_count` on `res.partner`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {integer}
|
||||
*/
|
||||
_mockResPartner_GetNeedactionCount(id) {
|
||||
const partner = this.getRecords('res.partner', [['id', '=', id]])[0];
|
||||
return this.getRecords('mail.notification', [
|
||||
['res_partner_id', '=', partner.id],
|
||||
['is_read', '=', false],
|
||||
]).length;
|
||||
},
|
||||
/**
|
||||
* Simulates `im_search` on `res.partner`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} [name='']
|
||||
* @param {integer} [limit=20]
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockResPartnerImSearch(name = '', limit = 20) {
|
||||
name = name.toLowerCase(); // simulates ILIKE
|
||||
// simulates domain with relational parts (not supported by mock server)
|
||||
const matchingPartners = this.getRecords('res.users', [])
|
||||
.filter(user => {
|
||||
const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
|
||||
// user must have a partner
|
||||
if (!partner) {
|
||||
return false;
|
||||
}
|
||||
// not current partner
|
||||
if (partner.id === this.currentPartnerId) {
|
||||
return false;
|
||||
}
|
||||
// no name is considered as return all
|
||||
if (!name) {
|
||||
return true;
|
||||
}
|
||||
if (partner.name && partner.name.toLowerCase().includes(name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).map(user => {
|
||||
const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
|
||||
return {
|
||||
id: partner.id,
|
||||
name: partner.name,
|
||||
};
|
||||
}).sort((a, b) => (a.name === b.name) ? (a.id - b.id) : (a.name > b.name) ? 1 : -1);
|
||||
matchingPartners.length = Math.min(matchingPartners.length, limit);
|
||||
return [...this._mockResPartnerMailPartnerFormat(matchingPartners.map(partner => partner.id)).values()];
|
||||
},
|
||||
/**
|
||||
* Simulates `mail_partner_format` on `res.partner`.
|
||||
*
|
||||
* @private
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Map}
|
||||
*/
|
||||
_mockResPartnerMailPartnerFormat(ids) {
|
||||
const partners = this.getRecords(
|
||||
'res.partner',
|
||||
[['id', 'in', ids]],
|
||||
{ active_test: false }
|
||||
);
|
||||
// Servers is also returning `is_internal_user` but not
|
||||
// done here for simplification.
|
||||
return new Map(partners.map(partner => {
|
||||
const users = this.getRecords('res.users', [['id', 'in', partner.user_ids]]);
|
||||
const internalUsers = users.filter(user => !user.share);
|
||||
let mainUser;
|
||||
if (internalUsers.length > 0) {
|
||||
mainUser = internalUsers[0];
|
||||
} else if (users.length > 0) {
|
||||
mainUser = users[0];
|
||||
}
|
||||
return [partner.id, {
|
||||
"active": partner.active,
|
||||
"email": partner.email,
|
||||
"id": partner.id,
|
||||
"im_status": partner.im_status,
|
||||
"name": partner.name,
|
||||
"user": mainUser ? {
|
||||
'id': mainUser.id,
|
||||
} : [['clear']],
|
||||
}];
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* Simulates `search_for_channel_invite` on `res.partner`.
|
||||
*
|
||||
* @private
|
||||
* @param {string} [search_term='']
|
||||
* @param {integer} [channel_id]
|
||||
* @param {integer} [limit=30]
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockResPartnerSearchForChannelInvite(search_term, channel_id, limit = 30) {
|
||||
search_term = search_term.toLowerCase(); // simulates ILIKE
|
||||
// simulates domain with relational parts (not supported by mock server)
|
||||
const matchingPartners = [...this._mockResPartnerMailPartnerFormat(
|
||||
this.getRecords('res.users', [])
|
||||
.filter(user => {
|
||||
const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0];
|
||||
// user must have a partner
|
||||
if (!partner) {
|
||||
return false;
|
||||
}
|
||||
// not current partner
|
||||
if (partner.id === this.currentPartnerId) {
|
||||
return false;
|
||||
}
|
||||
// no name is considered as return all
|
||||
if (!search_term) {
|
||||
return true;
|
||||
}
|
||||
if (partner.name && partner.name.toLowerCase().includes(search_term)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(user => user.partner_id)
|
||||
).values()];
|
||||
const count = matchingPartners.length;
|
||||
matchingPartners.length = Math.min(count, limit);
|
||||
return {
|
||||
count,
|
||||
partners: matchingPartners
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates `_message_fetch_failed` on `res.partner`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockResPartner_MessageFetchFailed(id) {
|
||||
const partner = this.getRecords('res.partner', [['id', '=', id]])[0];
|
||||
const messages = this.getRecords('mail.message', [
|
||||
['author_id', '=', partner.id],
|
||||
['res_id', '!=', 0],
|
||||
['model', '!=', false],
|
||||
['message_type', '!=', 'user_notification'],
|
||||
]).filter(message => {
|
||||
// Purpose is to simulate the following domain on mail.message:
|
||||
// ['notification_ids.notification_status', 'in', ['bounce', 'exception']],
|
||||
// But it's not supported by getRecords domain to follow a relation.
|
||||
const notifications = this.getRecords('mail.notification', [
|
||||
['mail_message_id', '=', message.id],
|
||||
['notification_status', 'in', ['bounce', 'exception']],
|
||||
]);
|
||||
return notifications.length > 0;
|
||||
});
|
||||
return this._mockMailMessage_MessageNotificationFormat(messages.map(message => message.id));
|
||||
},
|
||||
/**
|
||||
* Simulates `_get_channels_as_member` on `res.partner`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockResPartner_GetChannelsAsMember(ids) {
|
||||
const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0];
|
||||
const channelMembers = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id]]);
|
||||
const channels = this.getRecords('mail.channel', [
|
||||
['channel_type', 'in', ['channel', 'group']],
|
||||
['channel_member_ids', 'in', channelMembers.map(member => member.id)],
|
||||
]);
|
||||
const directMessagesMembers = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]);
|
||||
const directMessages = this.getRecords('mail.channel', [
|
||||
['channel_type', '=', 'chat'],
|
||||
['channel_member_ids', 'in', directMessagesMembers.map(member => member.id)],
|
||||
]);
|
||||
return [
|
||||
...channels,
|
||||
...directMessages,
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
import { date_to_str } from 'web.time';
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/res_users', {
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'res.users' && args.method === 'systray_get_activities') {
|
||||
return this._mockResUsersSystrayGetActivities();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates `_init_messaging` on `res.users`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockResUsers_InitMessaging(ids) {
|
||||
const user = this.getRecords('res.users', [['id', 'in', ids]])[0];
|
||||
const userSettings = this._mockResUsersSettings_FindOrCreateForUser(user.id);
|
||||
return {
|
||||
channels: this._mockMailChannelChannelInfo(this._mockResPartner_GetChannelsAsMember(user.partner_id).map(channel => channel.id)),
|
||||
current_partner: this._mockResPartnerMailPartnerFormat(user.partner_id).get(user.partner_id),
|
||||
current_user_id: this.currentUserId,
|
||||
current_user_settings: this._mockResUsersSettings_ResUsersSettingsFormat(userSettings.id),
|
||||
menu_id: false, // not useful in QUnit tests
|
||||
needaction_inbox_counter: this._mockResPartner_GetNeedactionCount(user.partner_id),
|
||||
partner_root: this._mockResPartnerMailPartnerFormat(this.partnerRootId).get(this.partnerRootId),
|
||||
shortcodes: this.pyEnv['mail.shortcode'].searchRead([], { fields: ['source', 'substitution'] }),
|
||||
starred_counter: this.getRecords('mail.message', [['starred_partner_ids', 'in', user.partner_id]]).length,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates `systray_get_activities` on `res.users`.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_mockResUsersSystrayGetActivities() {
|
||||
const activities = this.pyEnv['mail.activity'].searchRead([]);
|
||||
const userActivitiesByModelName = {};
|
||||
for (const activity of activities) {
|
||||
const today = date_to_str(new Date());
|
||||
if (today === activity['date_deadline']) {
|
||||
activity['states'] = 'today';
|
||||
} else if (today > activity['date_deadline']) {
|
||||
activity['states'] = 'overdue';
|
||||
} else {
|
||||
activity['states'] = 'planned';
|
||||
}
|
||||
}
|
||||
for (const activity of activities) {
|
||||
const modelName = activity['res_model'];
|
||||
if (!userActivitiesByModelName[modelName]) {
|
||||
userActivitiesByModelName[modelName] = {
|
||||
id: modelName, // for simplicity
|
||||
model: modelName,
|
||||
name: modelName,
|
||||
overdue_count: 0,
|
||||
planned_count: 0,
|
||||
today_count: 0,
|
||||
total_count: 0,
|
||||
type: 'activity',
|
||||
};
|
||||
}
|
||||
userActivitiesByModelName[modelName][`${activity['states']}_count`] += 1;
|
||||
userActivitiesByModelName[modelName]['total_count'] += 1;
|
||||
userActivitiesByModelName[modelName].actions = [{
|
||||
icon: 'fa-clock-o',
|
||||
name: 'Summary',
|
||||
}];
|
||||
}
|
||||
return Object.values(userActivitiesByModelName);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/res_users_settings', {
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'res.users.settings' && args.method === '_find_or_create_for_user') {
|
||||
const user_id = args.args[0][0];
|
||||
return this._mockResUsersSettings_FindOrCreateForUser(user_id);
|
||||
}
|
||||
if (args.model === 'res.users.settings' && args.method === 'set_res_users_settings') {
|
||||
const id = args.args[0][0];
|
||||
const newSettings = args.kwargs.new_settings;
|
||||
return this._mockResUsersSettingsSetResUsersSettings(id, newSettings);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulates `_find_or_create_for_user` on `res.users.settings`.
|
||||
*
|
||||
* @param {Object} user
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockResUsersSettings_FindOrCreateForUser(user_id) {
|
||||
let settings = this.getRecords('res.users.settings', [['user_id', '=', user_id]])[0];
|
||||
if (!settings) {
|
||||
const settingsId = this.pyEnv['res.users.settings'].create({ user_id: user_id });
|
||||
settings = this.getRecords('res.users.settings', [['id', '=', settingsId]])[0];
|
||||
}
|
||||
return settings;
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates `res_users_settings_format` on `res.users.settings`.
|
||||
*
|
||||
* @param {integer} id
|
||||
* @param {string[]} [fieldsToFormat]
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockResUsersSettings_ResUsersSettingsFormat(id, fieldsToFormat) {
|
||||
const [settings] = this.getRecords('res.users.settings', [['id', '=', id]]);
|
||||
const ormAutomaticFields = new Set(['create_date', 'create_uid', 'display_name', 'name', 'write_date', 'write_uid', '__last_update']);
|
||||
const filterPredicate = fieldsToFormat ? ([fieldName]) => fieldsToFormat.includes(fieldName) : ([fieldName]) => !ormAutomaticFields.has(fieldName);
|
||||
const res = Object.fromEntries(Object.entries(settings).filter(filterPredicate));
|
||||
if (Object.prototype.hasOwnProperty.call(res, 'user_id')) {
|
||||
res.user_id = { id: settings.user_id };
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(res, 'volume_settings_ids')) {
|
||||
const volumeSettings = this._mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat(settings.volume_settings_ids);
|
||||
res.volume_settings_ids = [['insert', volumeSettings]];
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates `set_res_users_settings` on `res.users.settings`.
|
||||
*
|
||||
* @param {integer} id
|
||||
* @param {Object} newSettings
|
||||
*/
|
||||
_mockResUsersSettingsSetResUsersSettings(id, newSettings) {
|
||||
const oldSettings = this.getRecords('res.users.settings', [['id', '=', id]])[0];
|
||||
const changedSettings = {};
|
||||
for (const setting in newSettings) {
|
||||
if (setting in oldSettings && newSettings[setting] !== oldSettings[setting]) {
|
||||
changedSettings[setting] = newSettings[setting];
|
||||
}
|
||||
}
|
||||
this.pyEnv['res.users.settings'].write(
|
||||
[id],
|
||||
changedSettings,
|
||||
);
|
||||
const [relatedUser] = this.pyEnv['res.users'].searchRead([['id', '=', oldSettings.user_id]]);
|
||||
const [relatedPartner] = this.pyEnv['res.partner'].searchRead([['id', '=', relatedUser.partner_id]]);
|
||||
this.pyEnv['bus.bus']._sendone(relatedPartner, 'res.users.settings/insert', { ...changedSettings, id });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
// ensure bus mock server is loaded first.
|
||||
import '@bus/../tests/helpers/mock_server';
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'mail/models/res_users_settings_volumes', {
|
||||
/**
|
||||
* Simulates `discuss_users_settings_volume_format` on `res.users.settings.volumes`.
|
||||
*
|
||||
* @param {Number[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat(ids) {
|
||||
const volumeSettingsRecords = this.getRecords('res.users.settings.volumes', [['id', 'in', ids]]);
|
||||
return volumeSettingsRecords.map(volumeSettingsRecord => {
|
||||
const [relatedGuest] = this.getRecords('mail.guest', [['id', '=', volumeSettingsRecord.guest_id]]);
|
||||
const [relatedPartner] = this.getRecords('res.partner', [['id', '=', volumeSettingsRecord.partner_id]]);
|
||||
return {
|
||||
guest_id: relatedGuest ? { id: relatedGuest.id, name: relatedGuest.name } : [['clear']],
|
||||
id: volumeSettingsRecord.id,
|
||||
partner_id: relatedPartner ? { id: relatedPartner.id, name: relatedPartner.name } : [['clear']],
|
||||
volume: volumeSettingsRecord.volume,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
// ensure mail override is applied first.
|
||||
import '@mail/../tests/helpers/mock_server';
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { MockServer } from '@web/../tests/helpers/mock_server';
|
||||
|
||||
import { date_to_str } from 'web.time';
|
||||
|
||||
patch(MockServer.prototype, 'note', {
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/note/new') {
|
||||
return this._mockRouteNoteNew(args);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private Mocked Routes
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simulates the `/note/new` route.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_mockRouteNoteNew(values) {
|
||||
const noteId = this.pyEnv['note.note'].create({ memo: values['note'] });
|
||||
if (values['date_deadline']) {
|
||||
this.pyEnv['mail.activity'].create({
|
||||
date_deadline: date_to_str(new Date(values['date_deadline'])),
|
||||
note_id: noteId,
|
||||
res_model: 'note.note',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private Mocked Methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simulates `systray_get_activities` on `res.users`.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_mockResUsersSystrayGetActivities() {
|
||||
const activities = this._super(...arguments);
|
||||
const noteCount = this.pyEnv['note.note'].searchCount([['user_id', '=', this.currentUserId]]);
|
||||
if (noteCount) {
|
||||
const noteIndex = activities.findIndex(act => act['model'] === 'note.note');
|
||||
if (noteIndex) {
|
||||
activities[noteIndex]['name'] = 'Notes';
|
||||
} else {
|
||||
activities.push({
|
||||
id: 'note.note', // for simplicity
|
||||
type: 'activity',
|
||||
name: 'Notes',
|
||||
model: 'note.note',
|
||||
planned_count: 0,
|
||||
today_count: 0,
|
||||
overdue_count: 0,
|
||||
total_count: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { MockServer } from '@web/../tests/helpers/mock_server';
|
||||
|
||||
import { date_to_str } from 'web.time';
|
||||
|
||||
patch(MockServer.prototype, 'note/controller/note', {
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/note/new') {
|
||||
return this._mockRouteNoteNew(args);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private Mocked Routes
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simulates the `/note/new` route.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_mockRouteNoteNew(values) {
|
||||
const noteId = this.pyEnv['note.note'].create({ memo: values['note'] });
|
||||
if (values['date_deadline']) {
|
||||
this.pyEnv['mail.activity'].create({
|
||||
date_deadline: date_to_str(new Date(values['date_deadline'])),
|
||||
note_id: noteId,
|
||||
res_model: 'note.note',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
// ensure mail override is applied first.
|
||||
import '@mail/../tests/helpers/mock_server/models/res_users';
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { MockServer } from '@web/../tests/helpers/mock_server';
|
||||
|
||||
patch(MockServer.prototype, 'note/models/res_users', {
|
||||
/**
|
||||
* Simulates `systray_get_activities` on `res.users`.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_mockResUsersSystrayGetActivities() {
|
||||
const activities = this._super(...arguments);
|
||||
const noteCount = this.pyEnv['note.note'].searchCount([['user_id', '=', this.currentUserId]]);
|
||||
if (noteCount) {
|
||||
const noteIndex = activities.findIndex(act => act['model'] === 'note.note');
|
||||
if (noteIndex) {
|
||||
activities[noteIndex]['name'] = 'Notes';
|
||||
} else {
|
||||
activities.push({
|
||||
id: 'note.note', // for simplicity
|
||||
type: 'activity',
|
||||
name: 'Notes',
|
||||
model: 'note.note',
|
||||
planned_count: 0,
|
||||
today_count: 0,
|
||||
overdue_count: 0,
|
||||
total_count: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
return activities;
|
||||
},
|
||||
});
|
||||
@@ -31,7 +31,7 @@ This module allows a customer to give rating.
|
||||
'rating/static/src/scss/rating_templates.scss',
|
||||
],
|
||||
'web.tests_assets': [
|
||||
'rating/static/tests/helpers/*.js',
|
||||
'rating/static/tests/helpers/**/*.js',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
|
||||
import '@mail/../tests/helpers/mock_server/models/mail_message'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'rating', {
|
||||
patch(MockServer.prototype, 'rating/models/mail_message', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -41,7 +41,7 @@ Allows users to send documents by post
|
||||
'snailmail/static/src/components/*/*.xml',
|
||||
],
|
||||
'web.tests_assets': [
|
||||
'snailmail/static/tests/helpers/*.js',
|
||||
'snailmail/static/tests/helpers/**/*.js',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'snailmail/static/tests/qunit_suite_tests/**/*.js',
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@mail/../tests/helpers/mock_server'; // ensure mail overrides are applied first
|
||||
import '@mail/../tests/helpers/mock_server/models/mail_message'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'snailmail', {
|
||||
patch(MockServer.prototype, 'snailmail/models/mail_message', {
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -13,7 +13,7 @@ patch(MockServer.prototype, 'snailmail', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async performRPC(route, args) {
|
||||
async _performRPC(route, args) {
|
||||
if (args.model === 'mail.message' && args.method === 'cancel_letter') {
|
||||
const ids = args.args[0];
|
||||
return this._mockMailMessageCancelLetter(ids);
|
||||
@@ -54,7 +54,7 @@ Allow website visitors to chat with the collaborators. This module also brings a
|
||||
'website_livechat/static/tests/tours/**/*',
|
||||
],
|
||||
'web.tests_assets': [
|
||||
'website_livechat/static/tests/helpers/*.js',
|
||||
'website_livechat/static/tests/helpers/**/*.js',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'website_livechat/static/tests/qunit_suite_tests/**/*.js',
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@im_livechat/../tests/helpers/mock_server'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'website_livechat', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/web/dataset/call_button') {
|
||||
return this._mockCallButton(args);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulate a 'call_button' operation from a view.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_mockCallButton({ args, kwargs, method, model }) {
|
||||
if (model === 'website.visitor' && method === 'action_send_chat_request') {
|
||||
return this._mockWebsiteVisitorActionSendChatRequest(args[0]);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* Overrides to add visitor information to livechat channels.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_mockMailChannelChannelInfo(ids) {
|
||||
const channelInfos = this._super(...arguments);
|
||||
for (const channelInfo of channelInfos) {
|
||||
const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
|
||||
if (channel.channel_type === 'livechat' && channelInfo.livechat_visitor_id) {
|
||||
const visitor = this.getRecords('website.visitor', [['id', '=', channelInfo.livechat_visitor_id]])[0];
|
||||
const country = this.getRecords('res.country', [['id', '=', visitor.country_id]])[0];
|
||||
channelInfo.visitor = {
|
||||
country_code: country && country.code,
|
||||
country_id: country && country.id,
|
||||
display_name: visitor.display_name,
|
||||
history: visitor.history, // TODO should be computed
|
||||
id: visitor.id,
|
||||
is_connected: visitor.is_connected,
|
||||
lang_name: visitor.lang_name,
|
||||
partner_id: visitor.partner_id,
|
||||
website_name: visitor.website_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
return channelInfos;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
*/
|
||||
_mockWebsiteVisitorActionSendChatRequest(ids) {
|
||||
const visitors = this.getRecords('website.visitor', [['id', 'in', ids]]);
|
||||
for (const visitor of visitors) {
|
||||
const country = visitor.country_id
|
||||
? this.getRecords('res.country', [['id', '=', visitor.country_id]])
|
||||
: undefined;
|
||||
const visitor_name = `${visitor.display_name}${country ? `(${country.name})` : ''}`;
|
||||
const membersToAdd = [[0, 0, { partner_id: this.currentPartnerId }]];
|
||||
if (visitor.partner_id) {
|
||||
membersToAdd.push([0, 0, { partner_id: visitor.partner_id }]);
|
||||
} else {
|
||||
membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
|
||||
}
|
||||
const livechatId = this.pyEnv['mail.channel'].create({
|
||||
anonymous_name: visitor_name,
|
||||
channel_member_ids: membersToAdd,
|
||||
channel_type: 'livechat',
|
||||
livechat_operator_id: this.currentPartnerId,
|
||||
});
|
||||
// notify operator
|
||||
this.pyEnv['bus.bus']._sendone(this.currentPartner, 'website_livechat.send_chat_request',
|
||||
this._mockMailChannelChannelInfo([livechatId])[0]
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'website_livechat/controllers/dataset', {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _performRPC(route, args) {
|
||||
if (route === '/web/dataset/call_button') {
|
||||
return this._mockCallButton(args);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
/**
|
||||
* Simulate a 'call_button' operation from a view.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_mockCallButton({ args, kwargs, method, model }) {
|
||||
if (model === 'website.visitor' && method === 'action_send_chat_request') {
|
||||
return this._mockWebsiteVisitorActionSendChatRequest(args[0]);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import '@im_livechat/../tests/helpers/mock_server/models/mail_channel'; // ensure mail overrides are applied first
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'website_livechat/models/mail_channel', {
|
||||
/**
|
||||
* Overrides to add visitor information to livechat channels.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_mockMailChannelChannelInfo(ids) {
|
||||
const channelInfos = this._super(...arguments);
|
||||
for (const channelInfo of channelInfos) {
|
||||
const channel = this.getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
|
||||
if (channel.channel_type === 'livechat' && channelInfo.livechat_visitor_id) {
|
||||
const visitor = this.getRecords('website.visitor', [['id', '=', channelInfo.livechat_visitor_id]])[0];
|
||||
const country = this.getRecords('res.country', [['id', '=', visitor.country_id]])[0];
|
||||
channelInfo.visitor = {
|
||||
country_code: country && country.code,
|
||||
country_id: country && country.id,
|
||||
display_name: visitor.display_name,
|
||||
history: visitor.history, // TODO should be computed
|
||||
id: visitor.id,
|
||||
is_connected: visitor.is_connected,
|
||||
lang_name: visitor.lang_name,
|
||||
partner_id: visitor.partner_id,
|
||||
website_name: visitor.website_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
return channelInfos;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, 'website_livechat/models/website_visitor', {
|
||||
/**
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
*/
|
||||
_mockWebsiteVisitorActionSendChatRequest(ids) {
|
||||
const visitors = this.getRecords('website.visitor', [['id', 'in', ids]]);
|
||||
for (const visitor of visitors) {
|
||||
const country = visitor.country_id
|
||||
? this.getRecords('res.country', [['id', '=', visitor.country_id]])
|
||||
: undefined;
|
||||
const visitor_name = `${visitor.display_name}${country ? `(${country.name})` : ''}`;
|
||||
const membersToAdd = [[0, 0, { partner_id: this.currentPartnerId }]];
|
||||
if (visitor.partner_id) {
|
||||
membersToAdd.push([0, 0, { partner_id: visitor.partner_id }]);
|
||||
} else {
|
||||
membersToAdd.push([0, 0, { partner_id: this.publicPartnerId }]);
|
||||
}
|
||||
const livechatId = this.pyEnv['mail.channel'].create({
|
||||
anonymous_name: visitor_name,
|
||||
channel_member_ids: membersToAdd,
|
||||
channel_type: 'livechat',
|
||||
livechat_operator_id: this.currentPartnerId,
|
||||
});
|
||||
// notify operator
|
||||
this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'website_livechat.send_chat_request',
|
||||
this._mockMailChannelChannelInfo([livechatId])[0]
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user