task-2438828 closes odoo/odoo#67227 X-original-commit: 49585b8ee726af7edd113c3012b1168d24dc4d7c Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
1816 lines
71 KiB
JavaScript
1816 lines
71 KiB
JavaScript
odoo.define('mail.MockServer', function (require) {
|
|
"use strict";
|
|
|
|
const { nextAnimationFrame } = require('mail/static/src/utils/test_utils.js');
|
|
|
|
const MockServer = require('web.MockServer');
|
|
|
|
MockServer.include({
|
|
/**
|
|
* Param 'data' may have keys for the different magic partners/users.
|
|
*
|
|
* Note: we must delete these keys, so that this is not
|
|
* handled as a model definition.
|
|
*
|
|
* @override
|
|
* @param {Object} [data.currentPartnerId]
|
|
* @param {Object} [data.currentUserId]
|
|
* @param {Object} [data.partnerRootId]
|
|
* @param {Object} [data.publicPartnerId]
|
|
* @param {Object} [data.publicUserId]
|
|
* @param {Widget} [options.widget] mocked widget (use to call services)
|
|
*/
|
|
init(data, options) {
|
|
if (data && data.currentPartnerId) {
|
|
this.currentPartnerId = data.currentPartnerId;
|
|
delete data.currentPartnerId;
|
|
}
|
|
if (data && data.currentUserId) {
|
|
this.currentUserId = data.currentUserId;
|
|
delete data.currentUserId;
|
|
}
|
|
if (data && data.partnerRootId) {
|
|
this.partnerRootId = data.partnerRootId;
|
|
delete data.partnerRootId;
|
|
}
|
|
if (data && data.publicPartnerId) {
|
|
this.publicPartnerId = data.publicPartnerId;
|
|
delete data.publicPartnerId;
|
|
}
|
|
if (data && data.publicUserId) {
|
|
this.publicUserId = data.publicUserId;
|
|
delete data.publicUserId;
|
|
}
|
|
this._widget = options.widget;
|
|
|
|
this._super(...arguments);
|
|
},
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Private
|
|
//--------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
async _performFetch(resource, init) {
|
|
if (resource === '/web/binary/upload_attachment') {
|
|
const formData = init.body;
|
|
const model = formData.get('model');
|
|
const id = parseInt(formData.get('id'));
|
|
const ufiles = formData.getAll('ufile');
|
|
const callback = formData.get('callback');
|
|
|
|
const attachmentIds = [];
|
|
for (const ufile of ufiles) {
|
|
const attachmentId = this._mockCreate('ir.attachment', {
|
|
// datas,
|
|
mimetype: ufile.type,
|
|
name: ufile.name,
|
|
res_id: id,
|
|
res_model: model,
|
|
});
|
|
attachmentIds.push(attachmentId);
|
|
}
|
|
const attachments = this._getRecords('ir.attachment', [['id', 'in', attachmentIds]]);
|
|
const formattedAttachments = attachments.map(attachment => {
|
|
return {
|
|
'filename': attachment.name,
|
|
'id': attachment.id,
|
|
'mimetype': attachment.mimetype,
|
|
'size': attachment.file_size
|
|
};
|
|
});
|
|
return {
|
|
text() {
|
|
return `
|
|
<script language="javascript" type="text/javascript">
|
|
var win = window.top.window;
|
|
win.jQuery(win).trigger('${callback}', ${JSON.stringify(formattedAttachments)});
|
|
</script>
|
|
`;
|
|
},
|
|
};
|
|
}
|
|
return this._super(...arguments);
|
|
},
|
|
/**
|
|
* @override
|
|
*/
|
|
async _performRpc(route, args) {
|
|
// routes
|
|
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);
|
|
}
|
|
if (route === '/mail/get_suggested_recipients') {
|
|
const model = args.model;
|
|
const res_ids = args.res_ids;
|
|
return this._mockRouteMailGetSuggestedRecipient(model, res_ids);
|
|
}
|
|
if (route === '/mail/init_messaging') {
|
|
return this._mockRouteMailInitMessaging();
|
|
}
|
|
if (route === '/mail/read_followers') {
|
|
return this._mockRouteMailReadFollowers(args);
|
|
}
|
|
if (route === '/mail/read_subscription_data') {
|
|
const follower_id = args.follower_id;
|
|
return this._mockRouteMailReadSubscriptionData(follower_id);
|
|
}
|
|
// mail.activity methods
|
|
if (args.model === 'mail.activity' && args.method === 'activity_format') {
|
|
let res = this._mockRead(args.model, args.args, args.kwargs);
|
|
res = res.map(function (record) {
|
|
if (record.mail_template_ids) {
|
|
record.mail_template_ids = record.mail_template_ids.map(function (template_id) {
|
|
return { id: template_id, name: "template" + template_id };
|
|
});
|
|
}
|
|
return record;
|
|
});
|
|
return res;
|
|
}
|
|
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);
|
|
}
|
|
// mail.channel methods
|
|
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_listeners') {
|
|
return [];
|
|
}
|
|
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 uuid = args.args[0] || args.kwargs.uuid;
|
|
const state = args.args[1] || args.kwargs.state;
|
|
return this._mockMailChannelChannelFold(uuid, 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 === 'channel_join_and_get_info') {
|
|
const ids = args.args[0];
|
|
return this._mockMailChannelChannelJoinAndGetInfo(ids);
|
|
}
|
|
if (args.model === 'mail.channel' && args.method === 'channel_minimize') {
|
|
return;
|
|
}
|
|
if (args.model === 'mail.channel' && args.method === 'channel_seen') {
|
|
const channel_ids = args.args[0];
|
|
const last_message_id = args.args[1] || args.kwargs.last_message_id;
|
|
return this._mockMailChannelChannelSeen(channel_ids, last_message_id);
|
|
}
|
|
if (args.model === 'mail.channel' && args.method === 'channel_set_custom_name') {
|
|
const channel_id = args.args[0] || args.kwargs.channel_id;
|
|
const name = args.args[1] || args.kwargs.name;
|
|
return this._mockMailChannelChannelSetCustomName(channel_id, name);
|
|
}
|
|
if (args.model === 'mail.channel' && args.method === 'execute_command') {
|
|
return this._mockMailChannelExecuteCommand(args);
|
|
}
|
|
if (args.model === 'mail.channel' && args.method === 'message_post') {
|
|
const id = args.args[0];
|
|
const kwargs = args.kwargs;
|
|
const context = kwargs.context;
|
|
delete kwargs.context;
|
|
return this._mockMailChannelMessagePost(id, kwargs, context);
|
|
}
|
|
if (args.model === 'mail.channel' && args.method === 'notify_typing') {
|
|
const ids = args.args[0];
|
|
const is_typing = args.args[1] || args.kwargs.is_typing;
|
|
const context = args.kwargs.context;
|
|
return this._mockMailChannelNotifyTyping(ids, is_typing, context);
|
|
}
|
|
// mail.message methods
|
|
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_fetch') {
|
|
// TODO FIXME delay RPC until next potential render as a workaround
|
|
// to issue https://github.com/odoo/owl/pull/724
|
|
await nextAnimationFrame();
|
|
const domain = args.args[0] || args.kwargs.domain;
|
|
const limit = args.args[1] || args.kwargs.limit;
|
|
const moderated_channel_ids = args.args[2] || args.kwargs.moderated_channel_ids;
|
|
return this._mockMailMessageMessageFetch(domain, limit, moderated_channel_ids);
|
|
}
|
|
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 === 'moderate') {
|
|
return this._mockMailMessageModerate(args);
|
|
}
|
|
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();
|
|
}
|
|
// res.partner methods
|
|
if (args.method === 'get_mention_suggestions') {
|
|
if (args.model === 'mail.channel') {
|
|
return this._mockMailChannelGetMentionSuggestions(args);
|
|
}
|
|
if (args.model === 'res.partner') {
|
|
return this._mockResPartnerGetMentionSuggestions(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);
|
|
}
|
|
// mail.thread methods (can work on any model)
|
|
if (args.method === 'message_subscribe') {
|
|
const ids = args.args[0];
|
|
const partner_ids = args.args[1] || args.kwargs.partner_ids;
|
|
const channel_ids = args.args[2] || args.kwargs.channel_ids;
|
|
const subtype_ids = args.args[3] || args.kwargs.subtype_ids;
|
|
return this._mockMailThreadMessageSubscribe(args.model, ids, partner_ids, channel_ids, subtype_ids);
|
|
}
|
|
if (args.method === 'message_unsubscribe') {
|
|
const ids = args.args[0];
|
|
const partner_ids = args.args[1] || args.kwargs.partner_ids;
|
|
const channel_ids = args.args[2] || args.kwargs.channel_ids;
|
|
return this._mockMailThreadMessageUnsubscribe(args.model, ids, partner_ids, channel_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);
|
|
}
|
|
return this._super(route, args);
|
|
},
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Private Mocked Routes
|
|
//--------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 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
|
|
);
|
|
},
|
|
/**
|
|
* Simulates `/mail/get_suggested_recipients` route.
|
|
*
|
|
* @private
|
|
* @returns {string} model
|
|
* @returns {integer[]} res_ids
|
|
* @returns {Object}
|
|
*/
|
|
_mockRouteMailGetSuggestedRecipient(model, res_ids) {
|
|
if (model === 'res.fake') {
|
|
return this._mockResFake_MessageGetSuggestedRecipients(model, res_ids);
|
|
}
|
|
return this._mockMailThread_MessageGetSuggestedRecipients(model, res_ids);
|
|
},
|
|
/**
|
|
* Simulates the `/mail/init_messaging` route.
|
|
*
|
|
* @private
|
|
* @returns {Object}
|
|
*/
|
|
_mockRouteMailInitMessaging() {
|
|
const channels = this._getRecords('mail.channel', [
|
|
['channel_type', '=', 'channel'],
|
|
['members', 'in', this.currentPartnerId],
|
|
['public', 'in', ['public', 'groups']],
|
|
]);
|
|
const channelInfos = this._mockMailChannelChannelInfo(channels.map(channel => channel.id));
|
|
|
|
const directMessages = this._getRecords('mail.channel', [
|
|
['channel_type', '=', 'chat'],
|
|
['is_pinned', '=', true],
|
|
['members', 'in', this.currentPartnerId],
|
|
]);
|
|
const directMessageInfos = this._mockMailChannelChannelInfo(directMessages.map(channel => channel.id));
|
|
|
|
const privateGroups = this._getRecords('mail.channel', [
|
|
['channel_type', '=', 'channel'],
|
|
['members', 'in', this.currentPartnerId],
|
|
['public', '=', 'private'],
|
|
]);
|
|
const privateGroupInfos = this._mockMailChannelChannelInfo(privateGroups.map(channel => channel.id));
|
|
|
|
const moderation_channel_ids = this._getRecords('mail.channel', [['is_moderator', '=', true]]).map(channel => channel.id);
|
|
const moderation_counter = this._getRecords('mail.message', [
|
|
['model', '=', 'mail.channel'],
|
|
['res_id', 'in', moderation_channel_ids],
|
|
['moderation_status', '=', 'pending_moderation'],
|
|
]).length;
|
|
|
|
const partnerRoot = this._getRecords(
|
|
'res.partner',
|
|
[['id', '=', this.partnerRootId]],
|
|
{ active_test: false }
|
|
)[0];
|
|
const partnerRootFormat = this._mockResPartnerMailPartnerFormat(partnerRoot.id);
|
|
|
|
const publicPartner = this._getRecords(
|
|
'res.partner',
|
|
[['id', '=', this.publicPartnerId]],
|
|
{ active_test: false }
|
|
)[0];
|
|
const publicPartnerFormat = this._mockResPartnerMailPartnerFormat(publicPartner.id);
|
|
|
|
const currentPartner = this._getRecords('res.partner', [['id', '=', this.currentPartnerId]])[0];
|
|
const currentPartnerFormat = this._mockResPartnerMailPartnerFormat(currentPartner.id);
|
|
|
|
const needaction_inbox_counter = this._mockResPartnerGetNeedactionCount();
|
|
|
|
const mailFailures = this._mockMailMessageMessageFetchFailed();
|
|
|
|
const shortcodes = this._getRecords('mail.shortcode', []);
|
|
|
|
const commands = this._getRecords('mail.channel_command', []);
|
|
|
|
const starredCounter = this._getRecords('mail.message', [
|
|
['starred_partner_ids', 'in', this.currentPartnerId],
|
|
]).length;
|
|
|
|
return {
|
|
channel_slots: {
|
|
channel_channel: channelInfos,
|
|
channel_direct_message: directMessageInfos,
|
|
channel_private_group: privateGroupInfos,
|
|
},
|
|
commands,
|
|
current_partner: currentPartnerFormat,
|
|
current_user_id: this.currentUserId,
|
|
mail_failures: mailFailures,
|
|
mention_partner_suggestions: [],
|
|
menu_id: false,
|
|
moderation_channel_ids,
|
|
moderation_counter,
|
|
needaction_inbox_counter,
|
|
partner_root: partnerRootFormat,
|
|
public_partners: [publicPartnerFormat],
|
|
shortcodes,
|
|
starred_counter: starredCounter,
|
|
};
|
|
},
|
|
/**
|
|
* Simulates the `/mail/read_followers` route.
|
|
*
|
|
* @private
|
|
* @param {integer[]} follower_ids
|
|
* @returns {Object} one key for list of followers and one for subtypes
|
|
*/
|
|
async _mockRouteMailReadFollowers(args) {
|
|
const res_id = args.res_id; // id of record to read the followers
|
|
const res_model = args.res_model; // model of record to read the followers
|
|
const followers = this._getRecords('mail.followers', [['res_id', '=', res_id], ['res_model', '=', res_model]]);
|
|
const currentPartnerFollower = followers.find(follower => follower.id === this.currentPartnerId);
|
|
const subtypes = currentPartnerFollower
|
|
? this._mockRouteMailReadSubscriptionData(currentPartnerFollower.id)
|
|
: false;
|
|
return { followers, subtypes };
|
|
},
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Private Mocked Methods
|
|
//--------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 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(function (type) {
|
|
let mailTemplates = [];
|
|
if (type.mail_template_ids) {
|
|
mailTemplates = type.mail_template_ids.map(function (id) {
|
|
const template = _.findWhere(self.data['mail.template'].records, { id: id });
|
|
return {
|
|
id: 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,
|
|
};
|
|
},
|
|
/**
|
|
* 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._widget.call('bus_service', 'trigger', 'notification', 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
|
|
// of not having `channel.partner`.
|
|
const channelInfos = this._mockMailChannelChannelInfo(ids);
|
|
for (const channelInfo of channelInfos) {
|
|
notifications.push([[false, 'res.partner', partner_id], 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', [['channel_ids', 'in', channel.id]]);
|
|
const lastMessage = channelMessages.reduce((lastMessage, message) => {
|
|
if (message.id > lastMessage.id) {
|
|
return message;
|
|
}
|
|
return lastMessage;
|
|
}, channelMessages[0]);
|
|
if (!lastMessage) {
|
|
continue;
|
|
}
|
|
this._mockWrite('mail.channel', [
|
|
[channel.id],
|
|
{ fetched_message_id: lastMessage.id },
|
|
]);
|
|
const notification = [
|
|
["dbName", 'mail.channel', channel.id],
|
|
{
|
|
id: `${channel.id}/${this.currentPartnerId}`, // simulate channel.partner id
|
|
info: 'channel_fetched',
|
|
last_message_id: lastMessage.id,
|
|
partner_id: this.currentPartnerId,
|
|
},
|
|
];
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
|
|
}
|
|
},
|
|
/**
|
|
* 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', [['channel_ids', 'in', 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 {string} uuid
|
|
* @param {state} [state]
|
|
*/
|
|
_mockMailChannelChannelFold(uuid, state) {
|
|
const channel = this._getRecords('mail.channel', [['uuid', '=', uuid]])[0];
|
|
this._mockWrite('mail.channel', [
|
|
[channel.id],
|
|
{
|
|
is_minimized: state !== 'closed',
|
|
state,
|
|
}
|
|
]);
|
|
const notifConfirmFold = [
|
|
["dbName", 'res.partner', this.currentPartnerId],
|
|
this._mockMailChannelChannelInfo([channel.id])[0]
|
|
];
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notifConfirmFold]);
|
|
},
|
|
/**
|
|
* 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._mockCreate('mail.channel', {
|
|
channel_type: 'chat',
|
|
mass_mailing: false,
|
|
is_minimized: true,
|
|
is_pinned: true,
|
|
members: [[6, 0, partners_to]],
|
|
name: partners.map(partner => partner.name).join(", "),
|
|
public: 'private',
|
|
state: 'open',
|
|
});
|
|
return this._mockMailChannelChannelInfo([id])[0];
|
|
},
|
|
/**
|
|
* Simulates `channel_info` on `mail.channel`.
|
|
*
|
|
* @private
|
|
* @param {integer[]} ids
|
|
* @param {string} [extra_info]
|
|
* @returns {Object[]}
|
|
*/
|
|
_mockMailChannelChannelInfo(ids, extra_info) {
|
|
const channels = this._getRecords('mail.channel', [['id', 'in', ids]]);
|
|
const all_partners = [...new Set(channels.reduce((all_partners, channel) => {
|
|
return [...all_partners, ...channel.members];
|
|
}, []))];
|
|
const direct_partners = [...new Set(channels.reduce((all_partners, channel) => {
|
|
if (channel.channel_type === 'chat') {
|
|
return [...all_partners, ...channel.members];
|
|
}
|
|
return all_partners;
|
|
}, []))];
|
|
const partnerInfos = this._mockMailChannelPartnerInfo(all_partners, direct_partners);
|
|
return channels.map(channel => {
|
|
const members = channel.members.map(partnerId => partnerInfos[partnerId]);
|
|
const messages = this._getRecords('mail.message', [
|
|
['channel_ids', 'in', [channel.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 res = Object.assign({}, channel, {
|
|
info: extra_info,
|
|
last_message_id: lastMessageId,
|
|
members,
|
|
message_needaction_counter: messageNeedactionCounter,
|
|
});
|
|
if (channel.channel_type === 'channel') {
|
|
delete res.members;
|
|
} else {
|
|
res['seen_partners_info'] = [{
|
|
partner_id: this.currentPartnerId,
|
|
seen_message_id: channel.seen_message_id,
|
|
fetched_message_id: channel.fetched_message_id,
|
|
}];
|
|
}
|
|
return res;
|
|
});
|
|
},
|
|
/**
|
|
* Simulates `channel_join_and_get_info` on `mail.channel`.
|
|
*
|
|
* @private
|
|
* @param {integer[]} ids
|
|
* @returns {Object[]}
|
|
*/
|
|
_mockMailChannelChannelJoinAndGetInfo(ids) {
|
|
const id = ids[0]; // ensure one
|
|
const channel = this._getRecords('mail.channel', [['id', '=', id]])[0];
|
|
// channel.partner not handled here for simplicity
|
|
if (!channel.is_pinned) {
|
|
this._mockWrite('mail.channel', [
|
|
[channel.id],
|
|
{ is_pinned: true },
|
|
]);
|
|
const body = `<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="${channel.id}">#${channel.name}</a></div>`;
|
|
const message_type = "notification";
|
|
const subtype_xmlid = "mail.mt_comment";
|
|
this._mockMailChannelMessagePost(
|
|
'mail.channel',
|
|
[channel.id],
|
|
{ body, message_type, subtype_xmlid },
|
|
);
|
|
}
|
|
// moderation_guidelines not handled here for simplicity
|
|
const channelInfo = this._mockMailChannelChannelInfo([channel.id], 'join')[0];
|
|
const notification = [[false, 'res.partner', this.currentPartnerId], channelInfo];
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
|
|
return channelInfo;
|
|
},
|
|
/**
|
|
* Simulates the `channel_seen` method of `mail.channel`.
|
|
*
|
|
* @private
|
|
* @param integer[] ids
|
|
* @param {integer} last_message_id
|
|
*/
|
|
async _mockMailChannelChannelSeen(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', [
|
|
['channel_ids', 'in', [channel.id]],
|
|
['id', '<=', last_message_id],
|
|
]);
|
|
if (!messagesBeforeGivenLastMessage || messagesBeforeGivenLastMessage.length === 0) {
|
|
return;
|
|
}
|
|
if (!channel) {
|
|
return;
|
|
}
|
|
if (channel.seen_message_id && channel.seen_message_id >= last_message_id) {
|
|
return;
|
|
}
|
|
this._mockMailChannel_SetLastSeenMessage([channel.id], last_message_id);
|
|
|
|
// Send notification
|
|
const payload = {
|
|
channel_id,
|
|
info: 'channel_seen',
|
|
last_message_id,
|
|
partner_id: this.currentPartnerId,
|
|
};
|
|
let notification;
|
|
if (channel.channel_type === 'chat') {
|
|
notification = [[false, 'mail.channel', channel_id], payload];
|
|
} else {
|
|
notification = [[false, 'res.partner', this.currentPartnerId], payload];
|
|
}
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
|
|
},
|
|
/**
|
|
* Simulates `channel_set_custom_name` on `mail.channel`.
|
|
*
|
|
* @private
|
|
* @param {integer} channel_id
|
|
* @returns {string} [name]
|
|
*/
|
|
_mockMailChannelChannelSetCustomName(channel_id, name) {
|
|
this._mockWrite('mail.channel', [
|
|
[channel_id],
|
|
{ custom_channel_name: name },
|
|
]);
|
|
},
|
|
/**
|
|
* Simulates `execute_command` on `mail.channel`.
|
|
* In particular sends a notification on the bus.
|
|
*
|
|
* @private
|
|
*/
|
|
_mockMailChannelExecuteCommand(args) {
|
|
const ids = args.args[0];
|
|
const commandName = args.kwargs.command || args.args[1];
|
|
const channels = this._getRecords('mail.channel', [['id', 'in', ids]]);
|
|
if (commandName === 'leave') {
|
|
for (const channel of channels) {
|
|
this._mockWrite('mail.channel', [
|
|
[channel.id],
|
|
{ is_pinned: false },
|
|
]);
|
|
const notifConfirmUnpin = [
|
|
["dbName", 'res.partner', this.currentPartnerId],
|
|
Object.assign({}, channel, { info: 'unsubscribe' })
|
|
];
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notifConfirmUnpin]);
|
|
}
|
|
return;
|
|
} else if (commandName === 'who') {
|
|
for (const channel of channels) {
|
|
const members = channel.members.map(memberId => this._getRecords('res.partner', [['id', '=', memberId]])[0].name);
|
|
let message = "You are alone in this channel.";
|
|
if (members.length > 0) {
|
|
message = `Users in this channel: ${members.join(', ')} and you`;
|
|
}
|
|
const notification = [
|
|
["dbName", 'res.partner', this.currentPartnerId],
|
|
{
|
|
'body': `<span class="o_mail_notification">${message}</span>`,
|
|
'channel_ids': [channel.id],
|
|
'info': 'transient_message',
|
|
}
|
|
];
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
|
|
}
|
|
return;
|
|
}
|
|
throw new Error(`mail/mock_server: the route execute_command doesn't implement the command "${commandName}"`);
|
|
},
|
|
/**
|
|
* 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 {
|
|
id: channel.id,
|
|
name: channel.name,
|
|
public: channel.public,
|
|
};
|
|
});
|
|
// reduce results to max limit
|
|
matchingChannels.length = Math.min(matchingChannels.length, limit);
|
|
return matchingChannels;
|
|
};
|
|
|
|
const mentionSuggestions = mentionSuggestionsFilter(this.data['mail.channel'].records, search, limit);
|
|
|
|
return mentionSuggestions;
|
|
},
|
|
/**
|
|
* Simulates `message_post` on `mail.channel`.
|
|
*
|
|
* For simplicity this mock handles a simple case in regard to moderation:
|
|
* - messages from JS are assumed to be always sent by the current partner,
|
|
* - moderation white list and black list are not checked.
|
|
*
|
|
* @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' && !channel.is_pinned) {
|
|
// channel.partner not handled here for simplicity
|
|
this._mockWrite('mail.channel', [
|
|
[channel.id],
|
|
{ is_pinned: true },
|
|
]);
|
|
}
|
|
let moderation_status = 'accepted';
|
|
if (channel.moderation && ['email', 'comment'].includes(message_type)) {
|
|
if (!channel.is_moderator) {
|
|
moderation_status = 'pending_moderation';
|
|
}
|
|
}
|
|
let channel_ids = [];
|
|
if (moderation_status === 'accepted') {
|
|
channel_ids = [[4, channel.id]];
|
|
}
|
|
const messageId = this._mockMailThreadMessagePost(
|
|
'mail.channel',
|
|
[id],
|
|
Object.assign(kwargs, {
|
|
channel_ids,
|
|
message_type,
|
|
moderation_status,
|
|
}),
|
|
context,
|
|
);
|
|
if (kwargs.author_id === this.currentPartnerId) {
|
|
this._mockMailChannel_SetLastSeenMessage([channel.id], messageId);
|
|
} else {
|
|
this._mockWrite('mail.channel', [
|
|
[channel.id],
|
|
{ message_unread_counter: (channel.message_unread_counter || 0) + 1 },
|
|
]);
|
|
}
|
|
return messageId;
|
|
},
|
|
/**
|
|
* Simulates `notify_typing` on `mail.channel`.
|
|
*
|
|
* @private
|
|
* @param {integer[]} ids
|
|
* @param {boolean} is_typing
|
|
* @param {Object} [context={}]
|
|
*/
|
|
_mockMailChannelNotifyTyping(ids, is_typing, context = {}) {
|
|
const channels = this._getRecords('mail.channel', [['id', 'in', ids]]);
|
|
let partner_id;
|
|
if ('mockedPartnerId' in context) {
|
|
partner_id = context.mockedPartnerId;
|
|
} else {
|
|
partner_id = this.currentPartnerId;
|
|
}
|
|
const partner = this._getRecords('res.partner', [['id', '=', partner_id]]);
|
|
const data = {
|
|
'info': 'typing_status',
|
|
'is_typing': is_typing,
|
|
'partner_id': partner_id,
|
|
'partner_name': partner.name,
|
|
};
|
|
const notifications = [];
|
|
for (const channel of channels) {
|
|
notifications.push([[false, 'mail.channel', channel.id], data]);
|
|
notifications.push([channel.uuid, data]); // notify livechat users
|
|
}
|
|
this._widget.call('bus_service', 'trigger', 'notification', notifications);
|
|
},
|
|
/**
|
|
* Simulates `_get_channel_partner_info` on `mail.channel`.
|
|
*
|
|
* @private
|
|
* @param {integer[]} all_partners
|
|
* @param {integer[]} direct_partners
|
|
* @returns {Object[]}
|
|
*/
|
|
_mockMailChannelPartnerInfo(all_partners, direct_partners) {
|
|
const partners = this._getRecords(
|
|
'res.partner',
|
|
[['id', 'in', all_partners]],
|
|
{ active_test: false },
|
|
);
|
|
const partnerInfos = {};
|
|
for (const partner of partners) {
|
|
const partnerInfo = {
|
|
email: partner.email,
|
|
id: partner.id,
|
|
name: partner.name,
|
|
};
|
|
if (direct_partners.includes(partner.id)) {
|
|
partnerInfo.im_status = partner.im_status;
|
|
}
|
|
partnerInfos[partner.id] = partnerInfo;
|
|
}
|
|
return partnerInfos;
|
|
},
|
|
/**
|
|
* Simulates the `_set_last_seen_message` method of `mail.channel`.
|
|
*
|
|
* @private
|
|
* @param {integer[]} ids
|
|
* @param {integer} message_id
|
|
*/
|
|
_mockMailChannel_SetLastSeenMessage(ids, message_id) {
|
|
this._mockWrite('mail.channel', [ids, {
|
|
fetched_message_id: message_id,
|
|
seen_message_id: message_id,
|
|
}]);
|
|
},
|
|
/**
|
|
* 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._mockWrite('mail.notification', [
|
|
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._mockWrite('mail.message', [
|
|
[message.id],
|
|
{
|
|
needaction: false,
|
|
needaction_partner_ids: message.needaction_partner_ids.filter(
|
|
partnerId => partnerId !== this.currentPartnerId
|
|
),
|
|
},
|
|
]);
|
|
}
|
|
const notificationData = { type: 'mark_as_read', message_ids: messageIds, needaction_inbox_counter: this._mockResPartnerGetNeedactionCount() };
|
|
const notification = [[false, 'res.partner', this.currentPartnerId], notificationData];
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
|
|
return messageIds;
|
|
},
|
|
/**
|
|
* Simulates `message_fetch` on `mail.message`.
|
|
*
|
|
* @private
|
|
* @param {Array[]} domain
|
|
* @param {string} [limit=20]
|
|
* @param {Object} [moderated_channel_ids]
|
|
* @returns {Object[]}
|
|
*/
|
|
_mockMailMessageMessageFetch(domain, limit = 20, moderated_channel_ids) {
|
|
let messages = this._getRecords('mail.message', domain);
|
|
if (moderated_channel_ids) {
|
|
const mod_messages = this._getRecords('mail.message', [
|
|
['model', '=', 'mail.channel'],
|
|
['res_id', 'in', moderated_channel_ids],
|
|
'|',
|
|
['author_id', '=', this.currentPartnerId],
|
|
['moderation_status', '=', 'pending_moderation'],
|
|
]);
|
|
messages = [...new Set([...messages, ...mod_messages])];
|
|
}
|
|
// 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 this._mockMailMessageMessageFormat(messages.map(message => message.id));
|
|
},
|
|
/**
|
|
* Simulates `message_fetch_failed` on `mail.message`.
|
|
*
|
|
* @private
|
|
* @returns {Object[]}
|
|
*/
|
|
_mockMailMessageMessageFetchFailed() {
|
|
const messages = this._getRecords('mail.message', [
|
|
['author_id', '=', this.currentPartnerId],
|
|
['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 `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 }
|
|
)[0];
|
|
formattedAuthor = [author.id, author.display_name];
|
|
} else {
|
|
formattedAuthor = [0, message.email_from];
|
|
}
|
|
const attachments = this._getRecords('ir.attachment', [
|
|
['id', 'in', message.attachment_ids],
|
|
]);
|
|
const formattedAttachments = attachments.map(attachment => {
|
|
return Object.assign({
|
|
'checksum': attachment.checksum,
|
|
'id': attachment.id,
|
|
'filename': attachment.name,
|
|
'name': attachment.name,
|
|
'mimetype': attachment.mimetype,
|
|
'is_main': thread && thread.message_main_attachment_id === attachment.id,
|
|
'res_id': attachment.res_id,
|
|
'res_model': attachment.res_model,
|
|
});
|
|
});
|
|
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 response = Object.assign({}, message, {
|
|
attachment_ids: formattedAttachments,
|
|
author_id: formattedAuthor,
|
|
history_partner_ids: historyPartnerIds,
|
|
needaction_partner_ids: needactionPartnerIds,
|
|
notifications,
|
|
tracking_value_ids: trackingValueIds,
|
|
});
|
|
if (message.subtype_id) {
|
|
const subtype = this._getRecords('mail.message.subtype', [
|
|
['id', '=', message.subtype_id],
|
|
])[0];
|
|
response.subtype_description = subtype.description;
|
|
}
|
|
return response;
|
|
});
|
|
},
|
|
/**
|
|
* Simulates `moderate` on `mail.message`.
|
|
*
|
|
* @private
|
|
*/
|
|
_mockMailMessageModerate(args) {
|
|
const messageIDs = args.args[0];
|
|
const decision = args.args[1];
|
|
const model = this.data['mail.message'];
|
|
if (decision === 'reject' || decision === 'discard') {
|
|
model.records = _.reject(model.records, function (rec) {
|
|
return _.contains(messageIDs, rec.id);
|
|
});
|
|
// simulate notification back (deletion of rejected/discarded
|
|
// message in channel)
|
|
const dbName = undefined; // useless for tests
|
|
const notifData = {
|
|
message_ids: messageIDs,
|
|
type: "deletion",
|
|
};
|
|
const metaData = [dbName, 'res.partner', this.currentPartnerId];
|
|
const notification = [metaData, notifData];
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
|
|
} else if (decision === 'accept') {
|
|
// simulate notification back (new accepted message in channel)
|
|
const messages = this._getRecords('mail.message', [['id', 'in', messageIDs]]);
|
|
for (const message of messages) {
|
|
this._mockWrite('mail.message', [[message.id], {
|
|
moderation_status: 'accepted',
|
|
}]);
|
|
this._mockMailThread_NotifyThread(model, message.channel_ids, message.id);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* 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._mockWrite('mail.notification', [
|
|
notifications.map(notification => notification.id),
|
|
{ is_read: true },
|
|
]);
|
|
// simulate compute that should be done based on notifications
|
|
for (const message of messages) {
|
|
this._mockWrite('mail.message', [
|
|
[message.id],
|
|
{
|
|
needaction: false,
|
|
needaction_partner_ids: message.needaction_partner_ids.filter(
|
|
partnerId => partnerId !== this.currentPartnerId
|
|
),
|
|
},
|
|
]);
|
|
// NOTE server is sending grouped notifications per channel_ids but
|
|
// this optimization is not needed here.
|
|
const data = { type: 'mark_as_read', message_ids: [message.id], channel_ids: message.channel_ids, needaction_inbox_counter: this._mockResPartnerGetNeedactionCount() };
|
|
const busNotifications = [[[false, 'res.partner', this.currentPartnerId], data]];
|
|
this._widget.call('bus_service', 'trigger', 'notification', busNotifications);
|
|
}
|
|
},
|
|
/**
|
|
* 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._mockWrite('mail.message', [
|
|
[message.id],
|
|
{ starred_partner_ids: [[wasStared ? 3 : 4, this.currentPartnerId]] }
|
|
]);
|
|
const notificationData = {
|
|
message_ids: [message.id],
|
|
starred: !wasStared,
|
|
type: 'toggle_star',
|
|
};
|
|
const notifications = [[[false, 'res.partner', this.currentPartnerId], notificationData]];
|
|
this._widget.call('bus_service', 'trigger', 'notification', notifications);
|
|
}
|
|
},
|
|
/**
|
|
* Simulates `unstar_all` on `mail.message`.
|
|
*
|
|
* @private
|
|
*/
|
|
_mockMailMessageUnstarAll() {
|
|
const messages = this._getRecords('mail.message', [
|
|
['starred_partner_ids', 'in', this.currentPartnerId],
|
|
]);
|
|
this._mockWrite('mail.message', [
|
|
messages.map(message => message.id),
|
|
{ starred_partner_ids: [[3, this.currentPartnerId]] }
|
|
]);
|
|
const notificationData = {
|
|
message_ids: messages.map(message => message.id),
|
|
starred: false,
|
|
type: 'toggle_star',
|
|
};
|
|
const notification = [[false, 'res.partner', this.currentPartnerId], notificationData];
|
|
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
|
|
},
|
|
/**
|
|
* Simulates `_filtered_for_web_client` on `mail.notification`.
|
|
*
|
|
* @private
|
|
* @returns {integer[]} ids
|
|
* @returns {Object[]}
|
|
*/
|
|
_mockMailNotification_FilteredForWebClient(ids) {
|
|
return this._getRecords('mail.notification', [
|
|
['id', 'in', ids],
|
|
['notification_type', '!=', 'inbox'],
|
|
['notification_status', 'in', ['bounce', 'exception', 'canceled']],
|
|
// or "res_partner_id.partner_share" not done here for simplicity
|
|
]);
|
|
},
|
|
/**
|
|
* 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.id, partner && partner.display_name],
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* 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) {
|
|
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.data[model].fields['user_id'].string;
|
|
this._mockMailThread_MessageAddSuggestedRecipient(result, user.partner_id, reason);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
/**
|
|
* 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,
|
|
'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,
|
|
'Email partner',
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
/**
|
|
* Simulates `message_post` on `mail.thread`.
|
|
*
|
|
* @private
|
|
* @param {string} model
|
|
* @param {integer[]} ids
|
|
* @param {Object} kwargs
|
|
* @param {Object} [context]
|
|
* @returns {integer}
|
|
*/
|
|
_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._mockWrite('ir.attachment', [
|
|
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._mockCreate('mail.message', values);
|
|
this._mockMailThread_NotifyThread(model, ids, messageId);
|
|
return messageId;
|
|
},
|
|
/**
|
|
* 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[]} channel_ids
|
|
* @param {integer[]} subtype_ids
|
|
* @returns {boolean}
|
|
*/
|
|
_mockMailThreadMessageSubscribe(model, ids, partner_ids, channel_ids, subtype_ids) {
|
|
// message_subscribe is too complex for a generic mock.
|
|
// mockRPC should be considered for a specific result.
|
|
},
|
|
/**
|
|
* 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 = [];
|
|
// author
|
|
const notificationData = {
|
|
type: 'author',
|
|
message: messageFormat,
|
|
};
|
|
if (message.author_id) {
|
|
notifications.push([[false, 'res.partner', message.author_id], notificationData]);
|
|
}
|
|
// members
|
|
const channels = this._getRecords('mail.channel', [['id', 'in', message.channel_ids]]);
|
|
for (const channel of channels) {
|
|
notifications.push([[false, 'mail.channel', channel.id], messageFormat]);
|
|
}
|
|
this._widget.call('bus_service', 'trigger', 'notification', 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
|
|
* @param {integer[]} channel_ids
|
|
* @returns {boolean|undefined}
|
|
*/
|
|
_mockMailThreadMessageUnsubscribe(model, ids, partner_ids, channel_ids) {
|
|
if (!partner_ids && !channel_ids) {
|
|
return true;
|
|
}
|
|
const followers = this._getRecords('mail.followers', [
|
|
['res_model', '=', model],
|
|
['res_id', 'in', ids],
|
|
'|',
|
|
['partner_id', 'in', partner_ids || []],
|
|
['channel_id', 'in', channel_ids || []],
|
|
]);
|
|
this._mockUnlink(model, [followers.map(follower => follower.id)]);
|
|
},
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* 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 = function (partners, search, limit) {
|
|
const matchingPartners = partners
|
|
.filter(partner => {
|
|
// 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 => {
|
|
// expected format
|
|
return {
|
|
email: partner.email,
|
|
id: partner.id,
|
|
name: partner.name,
|
|
};
|
|
});
|
|
// 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
|
|
if (mainMatchingPartners.length < limit) {
|
|
const partners = this._getRecords('res.partner', [['id', 'not in', mainMatchingPartners.map(partner => partner.id)]]);
|
|
extraMatchingPartners = mentionSuggestionsFilter(partners, search, limit);
|
|
}
|
|
return [mainMatchingPartners, extraMatchingPartners];
|
|
},
|
|
/**
|
|
* Simulates `get_needaction_count` on `res.partner`.
|
|
*
|
|
* @private
|
|
*/
|
|
_mockResPartnerGetNeedactionCount() {
|
|
return this._getRecords('mail.notification', [
|
|
['res_partner_id', '=', this.currentPartnerId],
|
|
['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,
|
|
im_status: user.im_status || 'offline',
|
|
name: partner.name,
|
|
user_id: user.id,
|
|
};
|
|
});
|
|
matchingPartners.length = Math.min(matchingPartners.length, limit);
|
|
return matchingPartners;
|
|
},
|
|
/**
|
|
* Simulates `mail_partner_format` on `res.partner`.
|
|
*
|
|
* @private
|
|
* @returns {integer} id
|
|
* @returns {Object}
|
|
*/
|
|
_mockResPartnerMailPartnerFormat(id) {
|
|
const partner = this._getRecords(
|
|
'res.partner',
|
|
[['id', '=', id]],
|
|
{ active_test: false }
|
|
)[0];
|
|
return {
|
|
"active": partner.active,
|
|
"display_name": partner.display_name,
|
|
"id": partner.id,
|
|
"im_status": partner.im_status,
|
|
"name": partner.name,
|
|
};
|
|
},
|
|
});
|
|
|
|
});
|