[FIX] mail, im_livechat: make livechat work properly
- fix traceback on receiving first livechat message due to bad computation of correspondent - fix missing [0] in return of country name_get in channel_info - fix visitor/correspondent computation to work when chatting with oneself - reintroduce livechat ordered by last message in discuss sidebar - improve mock server to make these fixes possible to test - introduce corresponding tests PR: #56243 task-2323670 closes odoo/odoo#56423 X-original-commit: f623bfd2c8819604c709bc723153c071dca6e3cc Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
@@ -89,7 +89,7 @@ class MailChannel(models.Model):
|
||||
channel_infos_dict[channel.id]['livechat_visitor'] = channel._channel_get_livechat_visitor_info()
|
||||
last_msg = self.env['mail.message'].search([("channel_ids", "in", [channel.id])], limit=1)
|
||||
if last_msg:
|
||||
channel_infos_dict[channel.id]['last_message_date'] = last_msg.date
|
||||
channel_infos_dict[channel.id]['last_message_id'] = last_msg.id
|
||||
return list(channel_infos_dict.values())
|
||||
|
||||
@api.model
|
||||
@@ -100,23 +100,24 @@ class MailChannel(models.Model):
|
||||
return values
|
||||
|
||||
def _channel_get_livechat_visitor_info(self):
|
||||
partners = self.channel_partner_ids - self.livechat_operator_id
|
||||
if partners:
|
||||
self.ensure_one()
|
||||
# remove active test to ensure public partner is taken into account
|
||||
channel_partner_ids = self.with_context(active_test=False).channel_partner_ids
|
||||
partners = channel_partner_ids - self.livechat_operator_id
|
||||
if not partners:
|
||||
# operator probably testing the livechat with his own user
|
||||
partners = channel_partner_ids
|
||||
if partners and partners[0] != self.env.ref('base.public_partner'):
|
||||
# legit non-public partner
|
||||
return {
|
||||
'country': partners[0].country_id.name_get() if partners[0].country_id else False,
|
||||
'country': partners[0].country_id.name_get()[0] if partners[0].country_id else False,
|
||||
'id': partners[0].id,
|
||||
'name': partners[0].name,
|
||||
}
|
||||
if self.anonymous_name:
|
||||
return {
|
||||
'country': False,
|
||||
'id': False,
|
||||
'name': self.anonymous_name,
|
||||
}
|
||||
return {
|
||||
'country': False,
|
||||
'country': self.country_id.name_get()[0] if self.country_id else False,
|
||||
'id': False,
|
||||
'name': _("Visitor"),
|
||||
'name': self.anonymous_name or _("Visitor"),
|
||||
}
|
||||
|
||||
def _channel_get_livechat_partner_name(self):
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
odoo.define('im_livechat/static/src/components/discuss/discuss_tests.js', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { afterEach, beforeEach, start } = require('mail/static/src/utils/test_utils.js');
|
||||
const {
|
||||
afterEach,
|
||||
afterNextRender,
|
||||
beforeEach,
|
||||
nextAnimationFrame,
|
||||
start,
|
||||
} = require('mail/static/src/utils/test_utils.js');
|
||||
|
||||
QUnit.module('im_livechat', {}, function () {
|
||||
QUnit.module('components', {}, function () {
|
||||
@@ -28,13 +34,12 @@ QUnit.module('discuss_tests.js', {
|
||||
QUnit.test('livechat in the sidebar: basic rendering', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
// channel that is expected to be found in the sidebar
|
||||
this.data['mail.channel'].records.push({
|
||||
channel_type: 'livechat', // channel is expected to be livechat
|
||||
id: 11, // random unique id, will be referenced in the test
|
||||
livechat_visitor: {
|
||||
name: "Visitor 11", // random name, will be asserted during the test
|
||||
},
|
||||
anonymous_name: "Visitor 11",
|
||||
channel_type: 'livechat',
|
||||
id: 11,
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, this.data.publicPartnerId],
|
||||
});
|
||||
await this.start();
|
||||
assert.containsOnce(document.body, '.o_Discuss_sidebar',
|
||||
@@ -69,6 +74,343 @@ QUnit.test('livechat in the sidebar: basic rendering', async function (assert) {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('livechat in the sidebar: existing user with country', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data['res.country'].records.push({
|
||||
code: 'be',
|
||||
id: 10,
|
||||
name: "Belgium",
|
||||
});
|
||||
this.data['res.partner'].records.push({
|
||||
country_id: 10,
|
||||
id: 10,
|
||||
name: "Jean",
|
||||
});
|
||||
this.data['mail.channel'].records.push({
|
||||
channel_type: 'livechat',
|
||||
id: 11,
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, 10],
|
||||
});
|
||||
await this.start();
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should have a channel group livechat in the side bar"
|
||||
);
|
||||
const livechat = document.querySelector('.o_DiscussSidebar_groupLivechat .o_DiscussSidebarItem');
|
||||
assert.ok(
|
||||
livechat,
|
||||
"should have a livechat in sidebar"
|
||||
);
|
||||
assert.strictEqual(
|
||||
livechat.textContent,
|
||||
"Jean (Belgium)",
|
||||
"should have user name and country as livechat name"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('do not add livechat in the sidebar on visitor opening his chat', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const currentUser = this.data['res.users'].records.find(user =>
|
||||
user.id === this.data.currentUserId
|
||||
);
|
||||
currentUser.im_status = 'online';
|
||||
this.data['im_livechat.channel'].records.push({
|
||||
id: 10,
|
||||
user_ids: [this.data.currentUserId],
|
||||
});
|
||||
await this.start();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should not have any livechat in the sidebar initially"
|
||||
);
|
||||
|
||||
// simulate livechat visitor opening his chat
|
||||
await this.env.services.rpc({
|
||||
route: '/im_livechat/get_session',
|
||||
params: {
|
||||
context: {
|
||||
mockedUserId: false,
|
||||
},
|
||||
channel_id: 10,
|
||||
},
|
||||
});
|
||||
await nextAnimationFrame();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should still not have any livechat in the sidebar after visitor opened his chat"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('do not add livechat in the sidebar on visitor typing', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const currentUser = this.data['res.users'].records.find(user =>
|
||||
user.id === this.data.currentUserId
|
||||
);
|
||||
currentUser.im_status = 'online';
|
||||
this.data['im_livechat.channel'].records.push({
|
||||
id: 10,
|
||||
user_ids: [this.data.currentUserId],
|
||||
});
|
||||
this.data['mail.channel'].records.push({
|
||||
channel_type: 'livechat',
|
||||
id: 10,
|
||||
is_pinned: false,
|
||||
livechat_channel_id: 10,
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.publicPartnerId, this.data.currentPartnerId],
|
||||
});
|
||||
await this.start();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should not have any livechat in the sidebar initially"
|
||||
);
|
||||
|
||||
// simulate livechat visitor typing
|
||||
const channel = this.data['mail.channel'].records.find(channel => channel.id === 10);
|
||||
await this.env.services.rpc({
|
||||
route: '/im_livechat/notify_typing',
|
||||
params: {
|
||||
context: {
|
||||
mockedPartnerId: this.publicPartnerId,
|
||||
},
|
||||
is_typing: true,
|
||||
uuid: channel.uuid,
|
||||
},
|
||||
});
|
||||
await nextAnimationFrame();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should still not have any livechat in the sidebar after visitor started typing"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('add livechat in the sidebar on visitor sending first message', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const currentUser = this.data['res.users'].records.find(user =>
|
||||
user.id === this.data.currentUserId
|
||||
);
|
||||
currentUser.im_status = 'online';
|
||||
this.data['res.country'].records.push({
|
||||
code: 'be',
|
||||
id: 10,
|
||||
name: "Belgium",
|
||||
});
|
||||
this.data['im_livechat.channel'].records.push({
|
||||
id: 10,
|
||||
user_ids: [this.data.currentUserId],
|
||||
});
|
||||
this.data['mail.channel'].records.push({
|
||||
anonymous_name: "Visitor (Belgium)",
|
||||
channel_type: 'livechat',
|
||||
country_id: 10,
|
||||
id: 10,
|
||||
is_pinned: false,
|
||||
livechat_channel_id: 10,
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.publicPartnerId, this.data.currentPartnerId],
|
||||
});
|
||||
await this.start();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should not have any livechat in the sidebar initially"
|
||||
);
|
||||
|
||||
// simulate livechat visitor sending a message
|
||||
const channel = this.data['mail.channel'].records.find(channel => channel.id === 10);
|
||||
await afterNextRender(async () => this.env.services.rpc({
|
||||
route: '/mail/chat_post',
|
||||
params: {
|
||||
context: {
|
||||
mockedUserId: false,
|
||||
},
|
||||
uuid: channel.uuid,
|
||||
message_content: "new message",
|
||||
},
|
||||
}));
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should have a channel group livechat in the side bar after receiving first message"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat .o_DiscussSidebar_item',
|
||||
"should have a livechat in the sidebar after receiving first message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_DiscussSidebar_groupLivechat .o_DiscussSidebar_item').textContent,
|
||||
"Visitor (Belgium)",
|
||||
"should have visitor name and country as livechat name"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('livechats are sorted by last message date in the sidebar: most recent at the top', async function (assert) {
|
||||
/**
|
||||
* For simplicity the code that is covered in this test is considering
|
||||
* messages to be more/less recent than others based on their ids instead of
|
||||
* their actual creation date.
|
||||
*/
|
||||
assert.expect(7);
|
||||
|
||||
this.data['mail.message'].records.push(
|
||||
{ id: 11, channel_ids: [11] }, // least recent message due to smaller id
|
||||
{ id: 12, channel_ids: [12] }, // most recent message due to higher id
|
||||
);
|
||||
this.data['mail.channel'].records.push(
|
||||
{
|
||||
anonymous_name: "Visitor 11",
|
||||
channel_type: 'livechat',
|
||||
id: 11,
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, this.data.publicPartnerId],
|
||||
},
|
||||
{
|
||||
anonymous_name: "Visitor 12",
|
||||
channel_type: 'livechat',
|
||||
id: 12,
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, this.data.publicPartnerId],
|
||||
},
|
||||
);
|
||||
await this.start();
|
||||
const livechat11 = this.env.models['mail.thread'].findFromIdentifyingData({
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const livechat12 = this.env.models['mail.thread'].findFromIdentifyingData({
|
||||
id: 12,
|
||||
model: 'mail.channel',
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should have a channel group livechat"
|
||||
);
|
||||
const initialLivechats = document.querySelectorAll('.o_DiscussSidebar_groupLivechat .o_DiscussSidebarItem');
|
||||
assert.strictEqual(
|
||||
initialLivechats.length,
|
||||
2,
|
||||
"should have 2 livechats in the sidebar"
|
||||
);
|
||||
assert.strictEqual(
|
||||
initialLivechats[0].dataset.threadLocalId,
|
||||
livechat12.localId,
|
||||
"first livechat should be the one with the most recent message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
initialLivechats[1].dataset.threadLocalId,
|
||||
livechat11.localId,
|
||||
"second livechat should be the one with the least recent message"
|
||||
);
|
||||
|
||||
// post a new message on the last channel
|
||||
await afterNextRender(() => initialLivechats[1].click());
|
||||
await afterNextRender(() => document.execCommand('insertText', false, "Blabla"));
|
||||
await afterNextRender(() => document.querySelector('.o_Composer_buttonSend').click());
|
||||
const livechats = document.querySelectorAll('.o_DiscussSidebar_groupLivechat .o_DiscussSidebarItem');
|
||||
assert.strictEqual(
|
||||
livechats.length,
|
||||
2,
|
||||
"should still have 2 livechats in the sidebar after posting a new message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
livechats[0].dataset.threadLocalId,
|
||||
livechat11.localId,
|
||||
"first livechat should now be the one on which the new message was posted"
|
||||
);
|
||||
assert.strictEqual(
|
||||
livechats[1].dataset.threadLocalId,
|
||||
livechat12.localId,
|
||||
"second livechat should now be the one on which the message was not posted"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('livechats with no messages are sorted by creation date in the sidebar: most recent at the top', async function (assert) {
|
||||
/**
|
||||
* For simplicity the code that is covered in this test is considering
|
||||
* channels to be more/less recent than others based on their ids instead of
|
||||
* their actual creation date.
|
||||
*/
|
||||
assert.expect(5);
|
||||
|
||||
this.data['mail.message'].records.push(
|
||||
{ id: 13, channel_ids: [13] },
|
||||
);
|
||||
this.data['mail.channel'].records.push(
|
||||
{
|
||||
anonymous_name: "Visitor 11",
|
||||
channel_type: 'livechat',
|
||||
id: 11, // least recent channel due to smallest id
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, this.data.publicPartnerId],
|
||||
},
|
||||
{
|
||||
anonymous_name: "Visitor 12",
|
||||
channel_type: 'livechat',
|
||||
id: 12, // most recent channel that does not have a message
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, this.data.publicPartnerId],
|
||||
},
|
||||
{
|
||||
anonymous_name: "Visitor 13",
|
||||
channel_type: 'livechat',
|
||||
id: 13, // most recent channel (but it has a message)
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, this.data.publicPartnerId],
|
||||
},
|
||||
);
|
||||
await this.start();
|
||||
const livechat11 = this.env.models['mail.thread'].findFromIdentifyingData({
|
||||
id: 11,
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const livechat12 = this.env.models['mail.thread'].findFromIdentifyingData({
|
||||
id: 12,
|
||||
model: 'mail.channel',
|
||||
});
|
||||
const livechat13 = this.env.models['mail.thread'].findFromIdentifyingData({
|
||||
id: 13,
|
||||
model: 'mail.channel',
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_DiscussSidebar_groupLivechat',
|
||||
"should have a channel group livechat"
|
||||
);
|
||||
const initialLivechats = document.querySelectorAll('.o_DiscussSidebar_groupLivechat .o_DiscussSidebarItem');
|
||||
assert.strictEqual(
|
||||
initialLivechats.length,
|
||||
3,
|
||||
"should have 3 livechats in the sidebar"
|
||||
);
|
||||
assert.strictEqual(
|
||||
initialLivechats[0].dataset.threadLocalId,
|
||||
livechat12.localId,
|
||||
"first livechat should be the most recent channel without message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
initialLivechats[1].dataset.threadLocalId,
|
||||
livechat11.localId,
|
||||
"second livechat should be the second most recent channel without message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
initialLivechats[2].dataset.threadLocalId,
|
||||
livechat13.localId,
|
||||
"third livechat should be the channel with a message"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,11 +19,27 @@ patch(components.DiscussSidebar, 'im_livechat/static/src/components/discuss_side
|
||||
* @returns {mail.thread[]}
|
||||
*/
|
||||
quickSearchOrderedAndPinnedLivechatList() {
|
||||
const allOrderedAndPinnedLivechats = this.env.models['mail.thread'].all(thread =>
|
||||
thread.channel_type === 'livechat' &&
|
||||
thread.isPinned &&
|
||||
thread.model === 'mail.channel'
|
||||
);
|
||||
const allOrderedAndPinnedLivechats = this.env.models['mail.thread']
|
||||
.all(thread =>
|
||||
thread.channel_type === 'livechat' &&
|
||||
thread.isPinned &&
|
||||
thread.model === 'mail.channel'
|
||||
).sort((c1, c2) => {
|
||||
// sort by: last message id (desc), id (desc)
|
||||
if (c1.lastMessage && c2.lastMessage) {
|
||||
return c2.lastMessage.id - c1.lastMessage.id;
|
||||
}
|
||||
// a channel without a last message is assumed to be a new
|
||||
// channel just created with the intent of posting a new
|
||||
// message on it, in which case it should be moved up.
|
||||
if (!c1.lastMessage) {
|
||||
return -1;
|
||||
}
|
||||
if (!c2.lastMessage) {
|
||||
return 1;
|
||||
}
|
||||
return c2.id - c1.id;
|
||||
});
|
||||
if (!this.discuss.sidebarQuickSearchValue) {
|
||||
return allOrderedAndPinnedLivechats;
|
||||
}
|
||||
@@ -59,7 +75,6 @@ patch(components.DiscussSidebar, 'im_livechat/static/src/components/discuss_side
|
||||
thread.isPinned &&
|
||||
thread.model === 'mail.channel'
|
||||
)
|
||||
.sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1)
|
||||
.map(livechat => livechat.__state),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -42,13 +42,12 @@ QUnit.module('thread_icon_tests.js', {
|
||||
QUnit.test('livechat: public website visitor is typing', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// channel that will be used for testing the typing feature
|
||||
this.data['mail.channel'].records.push({
|
||||
channel_type: 'livechat', // channel is expected to be livechat
|
||||
id: 20, // random unique id, will be referenced in the test
|
||||
livechat_visitor: {
|
||||
name: "Visitor 20", // random name, will be asserted during the test
|
||||
},
|
||||
anonymous_name: "Visitor 20",
|
||||
channel_type: 'livechat',
|
||||
id: 20,
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, this.data.publicPartnerId],
|
||||
});
|
||||
await this.start();
|
||||
const thread = this.env.models['mail.thread'].find(thread =>
|
||||
|
||||
+4
-7
@@ -42,15 +42,12 @@ QUnit.module('thread_textual_typing_status_tests.js', {
|
||||
QUnit.test('receive visitor typing status "is typing"', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// channel that will be used for testing the typing feature
|
||||
this.data['mail.channel'].records.push({
|
||||
// channel is expected to be livechat, but channel_type set only for
|
||||
// consistency, not actually useful in the scope of this test
|
||||
anonymous_name: "Visitor 20",
|
||||
channel_type: 'livechat',
|
||||
id: 20, // random unique id, will be referenced in the test
|
||||
livechat_visitor: {
|
||||
name: "Visitor 20", // random name, will be asserted during the test
|
||||
},
|
||||
id: 20,
|
||||
livechat_operator_id: this.data.currentPartnerId,
|
||||
members: [this.data.currentPartnerId, this.data.publicPartnerId],
|
||||
});
|
||||
await this.start();
|
||||
const thread = this.env.models['mail.thread'].find(thread =>
|
||||
|
||||
@@ -17,12 +17,35 @@ registerClassPatchModel('mail.thread', 'im_livechat/static/src/models/thread/thr
|
||||
*/
|
||||
convertData(data) {
|
||||
const data2 = this._super(data);
|
||||
if ('last_message_id' in data) {
|
||||
if (!data2.messagesAsServerChannel) {
|
||||
data2.messagesAsServerChannel = [];
|
||||
}
|
||||
data2.messagesAsServerChannel.push(['insert', { id: data.last_message_id }]);
|
||||
}
|
||||
if ('livechat_visitor' in data && data.livechat_visitor) {
|
||||
if (!data2.members) {
|
||||
data2.members = [];
|
||||
}
|
||||
// `livechat_visitor` without `id` is the anonymous visitor.
|
||||
if (!data.livechat_visitor.id) {
|
||||
// Create partner derived from public partner.
|
||||
/**
|
||||
* Create partner derived from public partner and replace the
|
||||
* public partner.
|
||||
*
|
||||
* Indeed the anonymous visitor is registered as a member of the
|
||||
* channel as the public partner in the database to avoid
|
||||
* polluting the contact list with many temporary partners.
|
||||
*
|
||||
* But the issue with public partner is that it is the same
|
||||
* record for every livechat, whereas every correspondent should
|
||||
* actually have its own visitor name, typing status, etc.
|
||||
*
|
||||
* Due to JS being temporary by nature there is no such notion
|
||||
* of polluting the database, it is therefore acceptable and
|
||||
* easier to handle one temporary partner per channel.
|
||||
*/
|
||||
data2.members.push(['unlink', this.env.messaging.publicPartner]);
|
||||
const partner = this.env.models['mail.partner'].create(
|
||||
Object.assign(
|
||||
this.env.models['mail.partner'].convertData(data.livechat_visitor),
|
||||
@@ -50,6 +73,9 @@ registerInstancePatchModel('mail.thread', 'im_livechat/static/src/models/thread/
|
||||
*/
|
||||
_computeDisplayName() {
|
||||
if (this.channel_type === 'livechat' && this.correspondent) {
|
||||
if (this.correspondent.country) {
|
||||
return `${this.correspondent.nameOrDisplayName} (${this.correspondent.country.name})`;
|
||||
}
|
||||
return this.correspondent.nameOrDisplayName;
|
||||
}
|
||||
return this._super();
|
||||
|
||||
@@ -15,9 +15,23 @@ MockModels.patch('im_livechat/static/tests/helpers/mock_models.js', T =>
|
||||
*/
|
||||
static generateData() {
|
||||
const data = super.generateData(...arguments);
|
||||
Object.assign(data, {
|
||||
'im_livechat.channel': {
|
||||
fields: {
|
||||
user_ids: { string: "Operators", type: 'many2many', relation: 'res.users' }
|
||||
},
|
||||
records: [],
|
||||
}
|
||||
});
|
||||
Object.assign(data['mail.channel'].fields, {
|
||||
// fake type, not on Python models but is the result of a formatter
|
||||
livechat_visitor: { string: 'livechat_visitor', type: 'Object', default: false },
|
||||
anonymous_name: { string: "Anonymous Name", type: 'char' },
|
||||
country_id: { string: "Country", type: 'many2one', relation: 'res.country' },
|
||||
livechat_active: { string: "Is livechat ongoing?", type: 'boolean', default: false },
|
||||
livechat_channel_id: { string: "Channel", type: 'many2one', relation: 'im_livechat.channel' },
|
||||
livechat_operator_id: { string: "Operator", type: 'many2one', relation: 'res.partner' },
|
||||
});
|
||||
Object.assign(data['res.users'].fields, {
|
||||
livechat_username: { string: 'Livechat Username', type: 'string' },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,76 @@ MockServer.include({
|
||||
// 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);
|
||||
}
|
||||
if (route === '/im_livechat/notify_typing') {
|
||||
const uuid = args.uuid;
|
||||
const is_typing = args.is_typing;
|
||||
const context = args.context;
|
||||
return this._mockRouteImLivechatNotifyTyping(uuid, is_typing, 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);
|
||||
},
|
||||
/**
|
||||
* Simulates the `/im_livechat/notify_typing` route.
|
||||
*
|
||||
* @private
|
||||
* @param {string} uuid
|
||||
* @param {boolean} is_typing
|
||||
* @param {Object} [context]
|
||||
*/
|
||||
_mockRouteImLivechatNotifyTyping(uuid, is_typing, context) {
|
||||
const mailChannel = this._getRecords('mail.channel', [['uuid', '=', uuid]])[0];
|
||||
this._mockMailChannelNotifyTyping([mailChannel.id], is_typing, context);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -27,6 +97,172 @@ MockServer.include({
|
||||
|
||||
return initMessaging;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private Mocked Methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simulates `_channel_get_livechat_visitor_info` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailChannel_ChannelGetLivechatVisitorInfo(ids) {
|
||||
const id = ids[0]; // ensure_one
|
||||
const mailChannel = this._getRecords('mail.channel', [['id', '=', id]])[0];
|
||||
// remove active test to ensure public partner is taken into account
|
||||
let members = this._getRecords(
|
||||
'res.partner',
|
||||
[['id', 'in', mailChannel.members]],
|
||||
{ active_test: false },
|
||||
);
|
||||
members = members.filter(member => member.id !== mailChannel.livechat_operator_id);
|
||||
if (members.length === 0 && mailChannel.livechat_operator_id) {
|
||||
// operator probably testing the livechat with his own user
|
||||
members = [mailChannel.livechat_operator_id];
|
||||
}
|
||||
if (members.length > 0 && members[0].id !== this.publicPartnerId) {
|
||||
// legit non-public partner
|
||||
const country = this._getRecords('res.country', [['id', '=', members[0].country_id]])[0];
|
||||
return {
|
||||
'country': country ? [country.id, country.name] : false,
|
||||
'id': members[0].id,
|
||||
'name': members[0].name,
|
||||
};
|
||||
}
|
||||
return {
|
||||
'country': false,
|
||||
'id': false,
|
||||
'name': mailChannel.anonymous_name || "Visitor",
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_mockMailChannelChannelInfo(ids, extra_info) {
|
||||
const channelInfos = this._super(...arguments);
|
||||
for (const channelInfo of channelInfos) {
|
||||
const channel = this._getRecords('mail.channel', [['id', '=', channelInfo.id]])[0];
|
||||
// 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(',', '')];
|
||||
}
|
||||
// add the anonymous or partner name
|
||||
channelInfo.livechat_visitor = this._mockMailChannel_ChannelGetLivechatVisitorInfo([channel.id]);
|
||||
const messages = this._getRecords('mail.message', [['channel_ids', 'in', channel.id]]);
|
||||
const last_msg = messages.sort((m1, m2) => m1.id > m2.id ? -1 : 1)[0];
|
||||
if (last_msg) {
|
||||
channelInfo.last_message_id = last_msg.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 members = [[4, 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) {
|
||||
// valid session user (not public)
|
||||
members.push([4, visitor_user.partner_id.id]);
|
||||
}
|
||||
} else {
|
||||
// for simplicity of not having mocked channel.partner, add public partner here
|
||||
members.push([4, this.publicPartnerId]);
|
||||
}
|
||||
const membersName = [
|
||||
visitor_user ? visitor_user.display_name : anonymous_name,
|
||||
operator.livechat_username ? operator.livechat_username : operator.name,
|
||||
];
|
||||
return {
|
||||
// Limitation of mocked models not having channel.partner: is_pinned
|
||||
// is not supposed to be false for the visitor but must be false for
|
||||
// the operator (writing on channel_partner_ids does not trigger the
|
||||
// defaults that would set it to true) and here operator and visitor
|
||||
// can't be differentiated in that regard.
|
||||
'is_pinned': false,
|
||||
'members': members, // channel_partner_ids
|
||||
'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(' '),
|
||||
'public': 'private',
|
||||
'mass_mailing': false, // email_send
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 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._mockCreate('mail.channel', mailChannelVals);
|
||||
this._mockMailChannel_broadcast([mailChannelId], [operator.partner_id]);
|
||||
return this._mockMailChannelChannelInfo([mailChannelId])[0];
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -52,6 +52,34 @@ class TestGetMailChannel(TransactionCase):
|
||||
channel_operator_ids = [channel_operator[0] for channel_operator in channel_operators]
|
||||
self.assertTrue(all(partner_id in channel_operator_ids for partner_id in self.operators.mapped('partner_id').ids))
|
||||
|
||||
def test_channel_get_livechat_visitor_info(self):
|
||||
belgium = self.env.ref('base.be')
|
||||
public_user = self.env.ref('base.public_user')
|
||||
test_user = self.env['res.users'].create({'name': 'Roger', 'login': 'roger', 'country_id': belgium.id})
|
||||
|
||||
# ensure visitor info are correct with anonymous
|
||||
channel_info = self.livechat_channel.with_user(public_user)._open_livechat_mail_channel(anonymous_name='Visitor 22', country_id=belgium.id)
|
||||
visitor_info = channel_info['livechat_visitor']
|
||||
self.assertFalse(visitor_info['id'])
|
||||
self.assertEqual(visitor_info['name'], "Visitor 22")
|
||||
self.assertEqual(visitor_info['country'], (20, "Belgium"))
|
||||
|
||||
# ensure visitor info are correct with real user
|
||||
channel_info = self.livechat_channel.with_user(test_user)._open_livechat_mail_channel(anonymous_name='whatever', user_id=test_user.id)
|
||||
visitor_info = channel_info['livechat_visitor']
|
||||
self.assertEqual(visitor_info['id'], test_user.partner_id.id)
|
||||
self.assertEqual(visitor_info['name'], "Roger")
|
||||
self.assertEqual(visitor_info['country'], (20, "Belgium"))
|
||||
|
||||
# ensure visitor info are correct when operator is testing himself
|
||||
operator = self.operators[0]
|
||||
channel_info = self.livechat_channel.with_user(operator)._open_livechat_mail_channel(anonymous_name='whatever', previous_operator_id=operator.partner_id.id, user_id=operator.id)
|
||||
self.assertEqual(channel_info['operator_pid'], (operator.partner_id.id, "Michel"))
|
||||
visitor_info = channel_info['livechat_visitor']
|
||||
self.assertEqual(visitor_info['id'], operator.partner_id.id)
|
||||
self.assertEqual(visitor_info['name'], "Michel")
|
||||
self.assertFalse(visitor_info['country'])
|
||||
|
||||
def _open_livechat_mail_channel(self):
|
||||
mail_channels = []
|
||||
|
||||
|
||||
@@ -607,12 +607,13 @@ function factory(dependencies) {
|
||||
tracking_value_ids: attr({
|
||||
default: [],
|
||||
}),
|
||||
|
||||
/**
|
||||
* All channels that this message is linked to (from server message
|
||||
* format).
|
||||
* All channels containing this message on the server.
|
||||
* Equivalent of python field `channel_ids`.
|
||||
*/
|
||||
serverChannels: many2many('mail.thread'),
|
||||
serverChannels: many2many('mail.thread', {
|
||||
inverse: 'messagesAsServerChannel',
|
||||
}),
|
||||
};
|
||||
|
||||
Message.modelName = 'mail.message';
|
||||
|
||||
+10
-5
@@ -56,7 +56,7 @@ function factory(dependencies) {
|
||||
/**
|
||||
* @private
|
||||
* @param {Object[]} notifications
|
||||
* @param {Array} notifications[i][0] meta-data of the notification.
|
||||
* @param {Array|string} notifications[i][0] meta-data of the notification.
|
||||
* @param {string} notifications[i][0][0] name of database this
|
||||
* notification comes from.
|
||||
* @param {string} notifications[i][0][1] type of notification.
|
||||
@@ -68,20 +68,25 @@ function factory(dependencies) {
|
||||
async _handleNotifications(notifications) {
|
||||
const filteredNotifications = this._filterNotificationsOnUnsubscribe(notifications);
|
||||
const proms = filteredNotifications.map(notification => {
|
||||
const [[, model, id], data] = notification;
|
||||
const [channel, message] = notification;
|
||||
if (typeof channel === 'string') {
|
||||
// uuid notification, only for (livechat) public handler
|
||||
return;
|
||||
}
|
||||
const [, model, id] = channel;
|
||||
switch (model) {
|
||||
case 'ir.needaction':
|
||||
return this._handleNotificationNeedaction(data);
|
||||
return this._handleNotificationNeedaction(message);
|
||||
case 'mail.channel':
|
||||
return this._handleNotificationChannel(Object.assign({
|
||||
channelId: id,
|
||||
}, data));
|
||||
}, message));
|
||||
case 'res.partner':
|
||||
if (id !== this.env.messaging.currentPartner.id) {
|
||||
// ignore broadcast to other partners
|
||||
return;
|
||||
}
|
||||
return this._handleNotificationPartner(Object.assign({}, data));
|
||||
return this._handleNotificationPartner(Object.assign({}, message));
|
||||
default:
|
||||
console.warn(`mail.messaging_notification_handler: Unhandled notification "${model}"`);
|
||||
return;
|
||||
|
||||
@@ -779,8 +779,13 @@ function factory(dependencies) {
|
||||
partner !== this.env.messaging.currentPartner
|
||||
);
|
||||
if (correspondents.length === 1) {
|
||||
// 2 members chat
|
||||
return [['link', correspondents[0]]];
|
||||
}
|
||||
if (this.members.length === 1) {
|
||||
// chat with oneself
|
||||
return [['link', this.members[0]]];
|
||||
}
|
||||
return [['unlink']];
|
||||
}
|
||||
|
||||
@@ -1360,6 +1365,13 @@ function factory(dependencies) {
|
||||
messages: many2many('mail.message', {
|
||||
inverse: 'threads',
|
||||
}),
|
||||
/**
|
||||
* All messages that are contained on this channel on the server.
|
||||
* Equivalent to the inverse of python field `channel_ids`.
|
||||
*/
|
||||
messagesAsServerChannel: many2many('mail.message', {
|
||||
inverse: 'serverChannels',
|
||||
}),
|
||||
messageSeenIndicators: one2many('mail.message_seen_indicator', {
|
||||
inverse: 'thread',
|
||||
isCausal: true,
|
||||
|
||||
@@ -260,33 +260,41 @@ const afterNextRender = (function () {
|
||||
function beforeEach(self) {
|
||||
const data = MockModels.generateData();
|
||||
|
||||
data.partnerRootId = 2;
|
||||
data['res.partner'].records.push({
|
||||
active: false,
|
||||
display_name: "OdooBot",
|
||||
id: 2,
|
||||
id: data.partnerRootId,
|
||||
});
|
||||
data.partnerRootId = 2;
|
||||
|
||||
data.currentPartnerId = 3;
|
||||
data['res.partner'].records.push({
|
||||
display_name: "Your Company, Mitchell Admin",
|
||||
id: 3,
|
||||
id: data.currentPartnerId,
|
||||
name: "Mitchell Admin",
|
||||
});
|
||||
data.currentPartnerId = 3;
|
||||
data.currentUserId = 2;
|
||||
data['res.users'].records.push({
|
||||
display_name: "Your Company, Mitchell Admin",
|
||||
id: 2,
|
||||
id: data.currentUserId,
|
||||
name: "Mitchell Admin",
|
||||
partner_id: data.currentPartnerId,
|
||||
});
|
||||
data.currentUserId = 2;
|
||||
|
||||
data.publicPartnerId = 4;
|
||||
data['res.partner'].records.push({
|
||||
active: false,
|
||||
display_name: "Public user",
|
||||
id: 4,
|
||||
id: data.publicPartnerId,
|
||||
});
|
||||
data.publicUserId = 3;
|
||||
data['res.users'].records.push({
|
||||
active: false,
|
||||
display_name: "Public user",
|
||||
id: data.publicUserId,
|
||||
name: "Public user",
|
||||
partner_id: data.publicPartnerId,
|
||||
});
|
||||
data.publicPartnerId = 4;
|
||||
|
||||
const originals = {
|
||||
'_.debounce': _.debounce,
|
||||
|
||||
@@ -133,12 +133,16 @@ QUnit.test('list activity widget with no activity', async function (assert) {
|
||||
QUnit.test('list activity widget with activities', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const lastUserIndex = this.data['res.users'].records.length - 1;
|
||||
this.data['res.users'].records[lastUserIndex].activity_ids = [1, 4];
|
||||
this.data['res.users'].records[lastUserIndex].activity_state = 'today';
|
||||
this.data['res.users'].records[lastUserIndex].activity_summary = 'Call with Al';
|
||||
this.data['res.users'].records[lastUserIndex].activity_type_id = 3;
|
||||
this.data['res.users'].records[lastUserIndex].activity_type_icon = 'fa-phone';
|
||||
const currentUser = this.data['res.users'].records.find(user =>
|
||||
user.id === this.data.currentUserId
|
||||
);
|
||||
Object.assign(currentUser, {
|
||||
activity_ids: [1, 4],
|
||||
activity_state: 'today',
|
||||
activity_summary: 'Call with Al',
|
||||
activity_type_id: 3,
|
||||
activity_type_icon: 'fa-phone',
|
||||
});
|
||||
|
||||
this.data['res.users'].records.push({
|
||||
id: 44,
|
||||
@@ -179,13 +183,17 @@ QUnit.test('list activity widget with activities', async function (assert) {
|
||||
QUnit.test('list activity widget with exception', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const lastUserIndex = this.data['res.users'].records.length - 1;
|
||||
this.data['res.users'].records[lastUserIndex].activity_ids = [1];
|
||||
this.data['res.users'].records[lastUserIndex].activity_state = 'today';
|
||||
this.data['res.users'].records[lastUserIndex].activity_summary = 'Call with Al';
|
||||
this.data['res.users'].records[lastUserIndex].activity_type_id = 3;
|
||||
this.data['res.users'].records[lastUserIndex].activity_exception_decoration = 'warning';
|
||||
this.data['res.users'].records[lastUserIndex].activity_exception_icon = 'fa-warning';
|
||||
const currentUser = this.data['res.users'].records.find(user =>
|
||||
user.id === this.data.currentUserId
|
||||
);
|
||||
Object.assign(currentUser, {
|
||||
activity_ids: [1],
|
||||
activity_state: 'today',
|
||||
activity_summary: 'Call with Al',
|
||||
activity_type_id: 3,
|
||||
activity_exception_decoration: 'warning',
|
||||
activity_exception_icon: 'fa-warning',
|
||||
});
|
||||
|
||||
const { widget: list } = await start({
|
||||
hasView: true,
|
||||
@@ -213,11 +221,15 @@ QUnit.test('list activity widget with exception', async function (assert) {
|
||||
QUnit.test('list activity widget: open dropdown', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const lastUserIndex = this.data['res.users'].records.length - 1;
|
||||
this.data['res.users'].records[lastUserIndex].activity_ids = [1, 4];
|
||||
this.data['res.users'].records[lastUserIndex].activity_state = 'today';
|
||||
this.data['res.users'].records[lastUserIndex].activity_summary = 'Call with Al';
|
||||
this.data['res.users'].records[lastUserIndex].activity_type_id = 3;
|
||||
const currentUser = this.data['res.users'].records.find(user =>
|
||||
user.id === this.data.currentUserId
|
||||
);
|
||||
Object.assign(currentUser, {
|
||||
activity_ids: [1, 4],
|
||||
activity_state: 'today',
|
||||
activity_summary: 'Call with Al',
|
||||
activity_type_id: 3,
|
||||
});
|
||||
this.data['mail.activity'].records.push(
|
||||
{
|
||||
id: 1,
|
||||
@@ -225,8 +237,8 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
|
||||
date_deadline: moment().format("YYYY-MM-DD"), // now
|
||||
can_write: true,
|
||||
state: "today",
|
||||
user_id: 2,
|
||||
create_uid: 2,
|
||||
user_id: this.data.currentUserId,
|
||||
create_uid: this.data.currentUserId,
|
||||
activity_type_id: 3,
|
||||
},
|
||||
{
|
||||
@@ -235,13 +247,13 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
|
||||
date_deadline: moment().add(1, 'day').format("YYYY-MM-DD"), // tomorrow
|
||||
can_write: true,
|
||||
state: "planned",
|
||||
user_id: 2,
|
||||
create_uid: 2,
|
||||
user_id: this.data.currentUserId,
|
||||
create_uid: this.data.currentUserId,
|
||||
activity_type_id: 1,
|
||||
}
|
||||
);
|
||||
|
||||
const { widget: list } = await start({
|
||||
const { env, widget: list } = await start({
|
||||
hasView: true,
|
||||
View: ListView,
|
||||
model: 'res.users',
|
||||
@@ -254,10 +266,15 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method || route);
|
||||
if (args.method === 'action_feedback') {
|
||||
this.data['res.users'].records[lastUserIndex].activity_ids = [4];
|
||||
this.data['res.users'].records[lastUserIndex].activity_state = 'planned';
|
||||
this.data['res.users'].records[lastUserIndex].activity_summary = 'Meet FP';
|
||||
this.data['res.users'].records[lastUserIndex].activity_type_id = 1;
|
||||
const currentUser = this.data['res.users'].records.find(user =>
|
||||
user.id === env.messaging.currentUser.id
|
||||
);
|
||||
Object.assign(currentUser, {
|
||||
activity_ids: [4],
|
||||
activity_state: 'planned',
|
||||
activity_summary: 'Meet FP',
|
||||
activity_type_id: 1,
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super(route, args);
|
||||
@@ -299,8 +316,10 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
|
||||
QUnit.test('list activity exception widget with activity', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const lastUserIndex = this.data['res.users'].records.length - 1;
|
||||
this.data['res.users'].records[lastUserIndex].activity_ids = [1];
|
||||
const currentUser = this.data['res.users'].records.find(user =>
|
||||
user.id === this.data.currentUserId
|
||||
);
|
||||
currentUser.activity_ids = [1];
|
||||
this.data['res.users'].records.push({
|
||||
id: 13,
|
||||
message_attachment_count: 3,
|
||||
@@ -390,8 +409,8 @@ QUnit.test('fieldmany2many tags email', function (assert) {
|
||||
assert.expect(13);
|
||||
var done = assert.async();
|
||||
|
||||
const lastUserIndex = this.data['res.users'].records.length - 1;
|
||||
this.data['res.users'].records[lastUserIndex].timmy = [12, 14];
|
||||
const user11 = this.data['res.users'].records.find(user => user.id === 11);
|
||||
user11.timmy = [12, 14];
|
||||
|
||||
// the modals need to be closed before the form view rendering
|
||||
start({
|
||||
@@ -451,8 +470,8 @@ QUnit.test('fieldmany2many tags email', function (assert) {
|
||||
QUnit.test('fieldmany2many tags email (edition)', async function (assert) {
|
||||
assert.expect(15);
|
||||
|
||||
const lastUserIndex = this.data['res.users'].records.length - 1;
|
||||
this.data['res.users'].records[lastUserIndex].timmy = [12];
|
||||
const user11 = this.data['res.users'].records.find(user => user.id === 11);
|
||||
user11.timmy = [12];
|
||||
|
||||
var { widget: form } = await start({
|
||||
hasView: true,
|
||||
@@ -512,12 +531,12 @@ QUnit.test('fieldmany2many tags email (edition)', async function (assert) {
|
||||
QUnit.test('many2many_tags_email widget can load more than 40 records', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const lastUserIndex = this.data['res.users'].records.length - 1;
|
||||
const user11 = this.data['res.users'].records.find(user => user.id === 11);
|
||||
this.data['res.users'].fields.partner_ids = { string: "Partner", type: "many2many", relation: 'res.users' };
|
||||
this.data['res.users'].records[lastUserIndex].partner_ids = [];
|
||||
user11.partner_ids = [];
|
||||
for (let i = 100; i < 200; i++) {
|
||||
this.data['res.users'].records.push({ id: i, display_name: `partner${i}` });
|
||||
this.data['res.users'].records[lastUserIndex].partner_ids.push(i);
|
||||
user11.partner_ids.push(i);
|
||||
}
|
||||
|
||||
const { widget: form } = await start({
|
||||
|
||||
@@ -117,6 +117,7 @@ class MockModels {
|
||||
body: { string: "Contents", type: 'html', default: "<p></p>" },
|
||||
channel_ids: { string: "Channels", type: 'many2many', relation: 'mail.channel' },
|
||||
date: { string: "Date", type: 'datetime' },
|
||||
email_from: { string: "From", type: 'char' },
|
||||
history_partner_ids: { string: "Partners with History", type: 'many2many', relation: 'res.partner' },
|
||||
id: { string: "Id", type: 'integer' },
|
||||
is_discussion: { string: "Discussion", type: 'boolean' },
|
||||
@@ -169,11 +170,19 @@ class MockModels {
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
'res.country': {
|
||||
fields: {
|
||||
code: { string: "Code", type: 'char' },
|
||||
name: { string: "Name", type: 'char' },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
'res.partner': {
|
||||
fields: {
|
||||
active: { string: "Active", type: 'boolean', default: true },
|
||||
activity_ids: { string: "Activities", type: 'one2many', relation: 'mail.activity' },
|
||||
contact_address_complete: { string: "Address", type: 'char' },
|
||||
country_id: { string: "Country", type: 'many2one', relation: 'res.country' },
|
||||
description: { string: 'description', type: 'text' },
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
email: { type: 'char' },
|
||||
@@ -192,6 +201,7 @@ class MockModels {
|
||||
fields: {
|
||||
active: { string: "Active", type: 'boolean', default: true },
|
||||
display_name: { string: "Display name", type: "char" },
|
||||
im_status: { string: "IM Status", type: 'char' },
|
||||
name: { string: "Name", type: 'char' },
|
||||
partner_id: { string: "Related partners", type: 'many2one', relation: 'res.partner' },
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ MockServer.include({
|
||||
* @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) {
|
||||
@@ -34,6 +35,10 @@ MockServer.include({
|
||||
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);
|
||||
@@ -92,6 +97,12 @@ MockServer.include({
|
||||
*/
|
||||
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/init_messaging') {
|
||||
return this._mockRouteMailInitMessaging();
|
||||
}
|
||||
@@ -146,6 +157,14 @@ MockServer.include({
|
||||
: 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;
|
||||
}
|
||||
@@ -163,10 +182,14 @@ MockServer.include({
|
||||
if (args.model === 'mail.channel' && args.method === 'message_post') {
|
||||
const id = args.args[0];
|
||||
const kwargs = args.kwargs;
|
||||
return this._mockMailChannelMessagePost(id, kwargs);
|
||||
const context = args.context;
|
||||
return this._mockMailChannelMessagePost(id, kwargs, context);
|
||||
}
|
||||
if (args.model === 'mail.channel' && args.method === 'notify_typing') {
|
||||
return;
|
||||
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') {
|
||||
@@ -222,7 +245,8 @@ MockServer.include({
|
||||
if (args.method === 'message_post') {
|
||||
const id = args.args[0];
|
||||
const kwargs = args.kwargs;
|
||||
return this._mockMailThreadMessagePost(id, args.model, kwargs);
|
||||
const context = args.kwargs.context;
|
||||
return this._mockMailThreadMessagePost(args.model, [id], kwargs, context);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
@@ -231,11 +255,60 @@ MockServer.include({
|
||||
// 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.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 the `/mail/init_messaging` route.
|
||||
*
|
||||
* @private
|
||||
* @return {Object}
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteMailInitMessaging() {
|
||||
const channels = this._getRecords('mail.channel', [
|
||||
@@ -302,7 +375,7 @@ MockServer.include({
|
||||
},
|
||||
commands: [],
|
||||
current_partner: currentPartnerFormat,
|
||||
curret_user_id: this.currentUserId,
|
||||
current_user_id: this.currentUserId,
|
||||
mail_failures: mailFailures,
|
||||
mention_partner_suggestions: [],
|
||||
menu_id: false,
|
||||
@@ -320,7 +393,7 @@ MockServer.include({
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} follower_ids
|
||||
* @return {Object} one key for list of followers and one for subtypes
|
||||
* @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
|
||||
@@ -337,7 +410,7 @@ MockServer.include({
|
||||
*
|
||||
* @private
|
||||
* @param {integer} follower_id
|
||||
* @return {Object[]} list of followed subtypes
|
||||
* @returns {Object[]} list of followed subtypes
|
||||
*/
|
||||
async _mockRouteMailReadSubscriptionData(follower_id) {
|
||||
const follower = this._getRecords('mail.followers', [['id', '=', follower_id]])[0];
|
||||
@@ -377,7 +450,7 @@ MockServer.include({
|
||||
* @private
|
||||
* @param {string} res_model
|
||||
* @param {string} domain
|
||||
* @return {Object}
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockMailActivityGetActivityData(res_model, domain) {
|
||||
const self = this;
|
||||
@@ -445,11 +518,49 @@ MockServer.include({
|
||||
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]]);
|
||||
@@ -485,7 +596,7 @@ MockServer.include({
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} ids
|
||||
* @return {Object[]} list of channels previews
|
||||
* @returns {Object[]} list of channels previews
|
||||
*/
|
||||
_mockMailChannelChannelFetchPreview(ids) {
|
||||
const channels = this._getRecords('mail.channel', [['id', 'in', ids]]);
|
||||
@@ -562,10 +673,11 @@ MockServer.include({
|
||||
* Simulates `channel_info` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer[]} [ids=[]]
|
||||
* @param {integer[]} ids
|
||||
* @param {string} [extra_info]
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailChannelChannelInfo(ids = []) {
|
||||
_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];
|
||||
@@ -590,17 +702,49 @@ MockServer.include({
|
||||
['mail_message_id', 'in', messages.map(message => message.id)],
|
||||
]).length;
|
||||
return Object.assign({}, channel, {
|
||||
info: extra_info,
|
||||
members,
|
||||
message_needaction_counter: messageNeedactionCounter,
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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 `channel_set_custom_name` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} channel_id
|
||||
* @return {string} [name]
|
||||
* @returns {string} [name]
|
||||
*/
|
||||
_mockMailChannelChannelSetCustomName(channel_id, name) {
|
||||
this._mockWrite('mail.channel', [
|
||||
@@ -642,27 +786,69 @@ MockServer.include({
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @param {Object} kwargs
|
||||
* @return {integer|false}
|
||||
* @param {Object} [context]
|
||||
* @returns {integer|false}
|
||||
*/
|
||||
_mockMailChannelMessagePost(id, kwargs) {
|
||||
_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(
|
||||
id,
|
||||
'mail.channel',
|
||||
[id],
|
||||
Object.assign(kwargs, {
|
||||
channel_ids,
|
||||
message_type,
|
||||
moderation_status,
|
||||
})
|
||||
}),
|
||||
context,
|
||||
);
|
||||
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 data = {
|
||||
'info': 'typing_status',
|
||||
'is_typing': is_typing,
|
||||
'partner_id': partner_id,
|
||||
};
|
||||
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 `partner_info` on `mail.channel`.
|
||||
*
|
||||
@@ -672,7 +858,11 @@ MockServer.include({
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailChannelPartnerInfo(all_partners, direct_partners) {
|
||||
const partners = this._getRecords('res.partner', [['id', 'in', all_partners]]);
|
||||
const partners = this._getRecords(
|
||||
'res.partner',
|
||||
[['id', 'in', all_partners]],
|
||||
{ active_test: false },
|
||||
);
|
||||
const partnerInfos = {};
|
||||
for (const partner of partners) {
|
||||
const partnerInfo = {
|
||||
@@ -692,7 +882,7 @@ MockServer.include({
|
||||
*
|
||||
* @private
|
||||
* @param {Array[]} [domain]
|
||||
* @return {integer[]}
|
||||
* @returns {integer[]}
|
||||
*/
|
||||
_mockMailMessageMarkAllAsRead(domain) {
|
||||
const notifDomain = [
|
||||
@@ -741,7 +931,7 @@ MockServer.include({
|
||||
* @param {Array[]} domain
|
||||
* @param {string} [limit=20]
|
||||
* @param {Object} [moderated_channel_ids]
|
||||
* @return {Object[]}
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailMessageMessageFetch(domain, limit = 20, moderated_channel_ids) {
|
||||
let messages = this._getRecords('mail.message', domain);
|
||||
@@ -767,7 +957,7 @@ MockServer.include({
|
||||
* Simulates `message_fetch_failed` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @return {Object[]}
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailMessageMessageFetchFailed() {
|
||||
const messages = this._getRecords('mail.message', [
|
||||
@@ -791,8 +981,8 @@ MockServer.include({
|
||||
* Simulates `message_format` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @return {integer[]} ids
|
||||
* @return {Object[]}
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailMessageMessageFormat(ids) {
|
||||
const messages = this._getRecords('mail.message', [['id', 'in', ids]]);
|
||||
@@ -806,7 +996,11 @@ MockServer.include({
|
||||
])[0];
|
||||
let formattedAuthor;
|
||||
if (message.author_id) {
|
||||
const author = this._getRecords('res.partner', [['id', '=', message.author_id]])[0];
|
||||
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];
|
||||
@@ -888,8 +1082,8 @@ MockServer.include({
|
||||
* Simulates `_message_notification_format` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @return {integer[]} ids
|
||||
* @return {Object[]}
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailMessage_MessageNotificationFormat(ids) {
|
||||
const messages = this._getRecords('mail.message', [['id', 'in', ids]]);
|
||||
@@ -957,7 +1151,7 @@ MockServer.include({
|
||||
* Simulates `toggle_message_starred` on `mail.message`.
|
||||
*
|
||||
* @private
|
||||
* @return {integer[]} ids
|
||||
* @returns {integer[]} ids
|
||||
*/
|
||||
_mockMailMessageToggleMessageStarred(ids) {
|
||||
const messages = this._getRecords('mail.message', [['id', 'in', ids]]);
|
||||
@@ -1001,8 +1195,8 @@ MockServer.include({
|
||||
* Simulates `_filtered_for_web_client` on `mail.notification`.
|
||||
*
|
||||
* @private
|
||||
* @return {integer[]} ids
|
||||
* @return {Object[]}
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailNotification_FilteredForWebClient(ids) {
|
||||
return this._getRecords('mail.notification', [
|
||||
@@ -1016,8 +1210,8 @@ MockServer.include({
|
||||
* Simulates `_notification_format` on `mail.notification`.
|
||||
*
|
||||
* @private
|
||||
* @return {integer[]} ids
|
||||
* @return {Object[]}
|
||||
* @returns {integer[]} ids
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
_mockMailNotification_NotificationFormat(ids) {
|
||||
const notifications = this._getRecords('mail.notification', [['id', 'in', ids]]);
|
||||
@@ -1032,16 +1226,71 @@ MockServer.include({
|
||||
};
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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_post` on `mail.thread`.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} id
|
||||
* @param {string} model
|
||||
* @param {integer[]} ids
|
||||
* @param {Object} kwargs
|
||||
* @return {integer}
|
||||
* @param {Object} [context]
|
||||
* @returns {integer}
|
||||
*/
|
||||
_mockMailThreadMessagePost(id, model, kwargs) {
|
||||
_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],
|
||||
@@ -1059,7 +1308,15 @@ MockServer.include({
|
||||
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,
|
||||
@@ -1067,13 +1324,7 @@ MockServer.include({
|
||||
});
|
||||
delete values.subtype_xmlid;
|
||||
const messageId = this._mockCreate('mail.message', values);
|
||||
const message = this._getRecords('mail.message', [['id', '=', messageId]])[0];
|
||||
const notificationData = {
|
||||
type: 'author',
|
||||
message: this._mockMailMessageMessageFormat([messageId])[0],
|
||||
};
|
||||
const notification = [[false, 'res.partner', message.author_id], notificationData];
|
||||
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
this._mockMailThread_NotifyThread(model, ids, messageId, values);
|
||||
return messageId;
|
||||
},
|
||||
/**
|
||||
@@ -1091,6 +1342,35 @@ MockServer.include({
|
||||
// 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`.
|
||||
*
|
||||
@@ -1223,8 +1503,8 @@ MockServer.include({
|
||||
* Simulates `mail_partner_format` on `res.partner`.
|
||||
*
|
||||
* @private
|
||||
* @return {integer} id
|
||||
* @return {Object}
|
||||
* @returns {integer} id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockResPartnerMailPartnerFormat(id) {
|
||||
const partner = this._getRecords(
|
||||
|
||||
Reference in New Issue
Block a user