[IMP] mail, *: move init messaging in models and avoid self.env.user

* = im_livechat, test_mail

The goal of moving it in models and making it independent of current user is to
be able to test it easily in future commits.

Part of task-2622462
This commit is contained in:
Sébastien Theys
2021-08-12 10:11:42 +00:00
parent 7582625ba7
commit 96d7102ee2
11 changed files with 199 additions and 206 deletions
-13
View File
@@ -71,19 +71,6 @@ class MailChannel(models.Model):
channel_infos_dict[channel.id]['livechat_visitor'] = channel._channel_get_livechat_visitor_info()
return list(channel_infos_dict.values())
@api.model
def channel_fetch_slot(self):
values = super(MailChannel, self).channel_fetch_slot()
livechat_channels = self.env['mail.channel'].search([
('channel_type', '=', 'livechat'),
('channel_last_seen_partner_ids', 'in', self.env['mail.channel.partner'].sudo()._search([
('partner_id', '=', self.env.user.partner_id.id),
('is_pinned', '=', True)])
),
])
values['channel_livechat'] = livechat_channels.channel_info()
return values
def _channel_get_livechat_visitor_info(self):
self.ensure_one()
# remove active test to ensure public partner is taken into account
+13 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, api
from odoo import models
class Partners(models.Model):
@@ -33,3 +33,15 @@ class Partners(models.Model):
else:
result = super(Partners, self).name_get()
return result
def _get_channels_as_member(self):
values = super()._get_channels_as_member()
livechat_channels = self.env['mail.channel'].search([
('channel_type', '=', 'livechat'),
('channel_last_seen_partner_ids', 'in', self.env['mail.channel.partner'].sudo()._search([
('partner_id', '=', self.id),
('is_pinned', '=', True),
])),
])
values['channel_livechat'] = livechat_channels.channel_info()
return values
@@ -79,23 +79,6 @@ MockServer.include({
const mailChannel = this._getRecords('mail.channel', [['uuid', '=', uuid]])[0];
this._mockMailChannelNotifyTyping([mailChannel.id], is_typing, context);
},
/**
* @override
*/
_mockRouteMailInitMessaging() {
const initMessaging = this._super(...arguments);
const livechats = this._getRecords('mail.channel', [
['channel_type', '=', 'livechat'],
['is_pinned', '=', true],
['members', 'in', this.currentPartnerId],
]);
initMessaging.channel_slots.channel_livechat = this._mockMailChannelChannelInfo(
livechats.map(channel => channel.id)
);
return initMessaging;
},
//--------------------------------------------------------------------------
// Private Mocked Methods
@@ -256,4 +239,19 @@ MockServer.include({
this._mockMailChannel_broadcast([mailChannelId], [operator.partner_id]);
return this._mockMailChannelChannelInfo([mailChannelId])[0];
},
/**
* @override
*/
_mockResPartner_GetChannelsAsMember(ids) {
const partner = this._getRecords('res.partner', [['id', 'in', ids]])[0];
const livechats = this._getRecords('mail.channel', [
['channel_type', '=', 'livechat'],
['is_pinned', '=', true],
['members', 'in', partner.id],
]);
return {
channel_livechat: this._mockMailChannelChannelInfo(livechats.map(channel => channel.id)),
...this._super(ids),
};
},
});
@@ -99,5 +99,4 @@ class TestGetMailChannel(TransactionCase):
self.assertFalse(operator_channel_partner.is_pinned, "channel should not be pinned for operator initially")
self.env['mail.channel'].browse(channel_info['id']).message_post(body='cc')
self.assertTrue(operator_channel_partner.is_pinned, "channel should be pinned for operator after visitor sent a message")
init_channels = self.env['mail.channel'].with_user(operator_channel_partner.partner_id.user_ids[0]).channel_fetch_slot()
self.assertIn(channel_info['id'], [init_channel_info['id'] for init_channel_info in init_channels['channel_livechat']], "channel should be fetched by operator on new page")
self.assertIn(channel_info['id'], [init_channel_info['id'] for init_channel_info in operator_channel_partner.partner_id._get_channels_as_member()['channel_livechat']], "channel should be fetched by operator on new page")
+2 -14
View File
@@ -250,23 +250,11 @@ class MailController(http.Controller):
@http.route('/mail/needaction', type='json', auth='user')
def needaction(self):
return request.env['res.partner'].get_needaction_count()
return request.env.user.partner_id._get_needaction_count()
@http.route('/mail/init_messaging', type='json', auth='user')
def mail_init_messaging(self):
values = {
'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(),
'starred_counter': request.env['res.partner'].get_starred_count(),
'channel_slots': request.env['mail.channel'].channel_fetch_slot(),
'mail_failures': request.env['mail.message'].message_fetch_failed(),
'shortcodes': request.env['mail.shortcode'].sudo().search_read([], ['source', 'substitution', 'description']),
'menu_id': request.env['ir.model.data']._xmlid_to_res_id('mail.menu_root_discuss'),
'partner_root': request.env.ref('base.partner_root').sudo().mail_partner_format(),
'public_partners': [partner.mail_partner_format() for partner in request.env.ref('base.group_public').sudo().with_context(active_test=False).users.partner_id],
'current_partner': request.env.user.partner_id.mail_partner_format(),
'current_user_id': request.env.user.id,
}
return values
return request.env.user._init_messaging()
@http.route('/mail/get_partner_info', type='json', auth='user')
def message_partner_info_from_emails(self, model, res_ids, emails, link_mail=False):
-21
View File
@@ -822,27 +822,6 @@ class Channel(models.Model):
# IM VIEW SPECIFIC (Slack Client Action)
# ------------------------------------------------------------
@api.model
def channel_fetch_slot(self):
""" Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and
the mapping between partner_id/channel_id for direct_message channels.
:returns dict : the grouped channels and the mapping
"""
values = {}
my_partner_id = self.env.user.partner_id.id
pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', my_partner_id), ('is_pinned', '=', True)]).mapped('channel_id')
# get the group/public channels
values['channel_channel'] = self.search([('channel_type', '=', 'channel'), ('public', 'in', ['public', 'groups']), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info()
# get the pinned 'direct message' channel
direct_message_channels = self.search([('channel_type', '=', 'chat'), ('id', 'in', pinned_channels.ids)])
values['channel_direct_message'] = direct_message_channels.channel_info()
# get the private group
values['channel_private_group'] = self.search([('channel_type', '=', 'channel'), ('public', '=', 'private'), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info()
return values
@api.model
def channel_search_to_join(self, name=None, domain=None):
""" Return the channel info of the channel the current partner can join
+3 -17
View File
@@ -6,12 +6,10 @@ import re
from collections import defaultdict
from binascii import Error as binascii_error
from operator import itemgetter
from odoo import _, api, Command, fields, models, modules, tools
from odoo.exceptions import AccessError, UserError
from odoo.exceptions import AccessError
from odoo.osv import expression
from odoo.tools import groupby
from odoo.tools.misc import clean_context
_logger = logging.getLogger(__name__)
@@ -682,7 +680,7 @@ class Message(models.Model):
ids = [n['mail_message_id'] for n in notifications.read(['mail_message_id'])]
notification = {'type': 'mark_as_read', 'message_ids': [id[0] for id in ids], 'needaction_inbox_counter': self.env.user.partner_id.get_needaction_count()}
notification = {'type': 'mark_as_read', 'message_ids': [id[0] for id in ids], 'needaction_inbox_counter': self.env.user.partner_id._get_needaction_count()}
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id), notification)
return ids
@@ -706,7 +704,7 @@ class Message(models.Model):
(self._cr.dbname, 'res.partner', partner_id.id),
{'type': 'mark_as_read',
'message_ids': notifications.mapped('mail_message_id').ids,
'needaction_inbox_counter': self.env.user.partner_id.get_needaction_count()
'needaction_inbox_counter': self.env.user.partner_id._get_needaction_count(),
}
)
@@ -800,18 +798,6 @@ class Message(models.Model):
return vals_list
def message_fetch_failed(self):
"""Returns all messages, sent by the current user, that have errors, in
the format expected by the web client."""
messages = self.search([
('has_error', '=', True),
('author_id', '=', self.env.user.partner_id.id),
('res_id', '!=', 0),
('model', '!=', False),
('message_type', '!=', 'user_notification')
])
return messages._message_notification_format()
@api.model
def message_fetch(self, domain, limit=20):
""" Get a limited amount of formatted messages with provided domain.
+58 -24
View File
@@ -6,7 +6,6 @@ import logging
from odoo import _, api, fields, models, tools
from odoo.addons.bus.models.bus_presence import AWAY_TIMER
from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER
from odoo.exceptions import AccessError
from odoo.osv import expression
_logger = logging.getLogger(__name__)
@@ -87,30 +86,65 @@ class Partner(models.Model):
res["is_internal_user"] = not main_user.share
return res
@api.model
def get_needaction_count(self):
""" compute the number of needaction of the current user """
if self.env.user.partner_id:
self.env['mail.notification'].flush(['is_read', 'res_partner_id'])
self.env.cr.execute("""
SELECT count(*) as needaction_count
FROM mail_notification R
WHERE R.res_partner_id = %s AND (R.is_read = false OR R.is_read IS NULL)""", (self.env.user.partner_id.id,))
return self.env.cr.dictfetchall()[0].get('needaction_count')
_logger.error('Call to needaction_count without partner_id')
return 0
def _get_needaction_count(self):
""" compute the number of needaction of the current partner """
self.ensure_one()
self.env['mail.notification'].flush(['is_read', 'res_partner_id'])
self.env.cr.execute("""
SELECT count(*) as needaction_count
FROM mail_notification R
WHERE R.res_partner_id = %s AND (R.is_read = false OR R.is_read IS NULL)""", (self.id,))
return self.env.cr.dictfetchall()[0].get('needaction_count')
@api.model
def get_starred_count(self):
""" compute the number of starred of the current user """
if self.env.user.partner_id:
self.env.cr.execute("""
SELECT count(*) as starred_count
FROM mail_message_res_partner_starred_rel R
WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id,))
return self.env.cr.dictfetchall()[0].get('starred_count')
_logger.error('Call to starred_count without partner_id')
return 0
def _get_starred_count(self):
""" compute the number of starred of the current partner """
self.ensure_one()
self.env.cr.execute("""
SELECT count(*) as starred_count
FROM mail_message_res_partner_starred_rel R
WHERE R.res_partner_id = %s """, (self.id,))
return self.env.cr.dictfetchall()[0].get('starred_count')
def _message_fetch_failed(self):
"""Returns all messages, sent by the current partner, that have errors, in
the format expected by the web client."""
self.ensure_one()
messages = self.env['mail.message'].search([
('has_error', '=', True),
('author_id', '=', self.id),
('res_id', '!=', 0),
('model', '!=', False),
('message_type', '!=', 'user_notification')
])
return messages._message_notification_format()
def _get_channels_as_member(self):
""" Return the channels of the partner grouped by 'slot' (channel, direct_message or private_group), and
the mapping between partner_id/channel_id for direct_message channels.
:returns dict : the grouped channels and the mapping
"""
self.ensure_one()
values = {}
pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.id), ('is_pinned', '=', True)]).mapped('channel_id')
# get the group/public channels
values['channel_channel'] = self.env['mail.channel'].search([
('channel_type', '=', 'channel'),
('public', 'in', ['public', 'groups']),
('channel_partner_ids', 'in', [self.id]),
]).channel_info()
# get the pinned 'direct message' channel
direct_message_channels = self.env['mail.channel'].search([
('channel_type', '=', 'chat'),
('id', 'in', pinned_channels.ids),
])
values['channel_direct_message'] = direct_message_channels.channel_info()
# get the private group
values['channel_private_group'] = self.env['mail.channel'].search([
('channel_type', '=', 'channel'),
('public', '=', 'private'),
('channel_partner_ids', 'in', [self.id]),
]).channel_info()
return values
@api.model
def search_for_channel_invite(self, search_term, channel_id=None, limit=30):
+17 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, exceptions, fields, models, modules, Command
from odoo import _, api, exceptions, fields, models, modules
from odoo.addons.base.models.res_users import is_selection_groups
@@ -80,6 +80,22 @@ class Users(models.Model):
lambda cp: cp.channel_id.public != 'public' and cp.channel_id.channel_type == 'channel'
).unlink()
def _init_messaging(self):
self.ensure_one()
values = {
'channel_slots': self.partner_id._get_channels_as_member(),
'current_partner': self.partner_id.mail_partner_format(),
'current_user_id': self.id,
'mail_failures': self.partner_id._message_fetch_failed(),
'menu_id': self.env['ir.model.data']._xmlid_to_res_id('mail.menu_root_discuss'),
'needaction_inbox_counter': self.partner_id._get_needaction_count(),
'partner_root': self.env.ref('base.partner_root').sudo().mail_partner_format(),
'public_partners': [partner.mail_partner_format() for partner in self.env.ref('base.group_public').sudo().with_context(active_test=False).users.partner_id],
'shortcodes': self.env['mail.shortcode'].sudo().search_read([], ['source', 'substitution', 'description']),
'starred_counter': self.partner_id._get_starred_count(),
}
return values
@api.model
def systray_get_activities(self):
query = """SELECT m.id, count(*), act.res_model as model,
+87 -93
View File
@@ -356,70 +356,7 @@ MockServer.include({
* @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 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 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,
},
current_partner: currentPartnerFormat,
current_user_id: this.currentUserId,
mail_failures: mailFailures,
menu_id: false,
needaction_inbox_counter,
partner_root: partnerRootFormat,
public_partners: [publicPartnerFormat],
shortcodes,
starred_counter: starredCounter,
};
return this._mockResUsers_InitMessaging([this.currentUserId]);
},
/**
* Simulates the `/mail/read_followers` route.
@@ -1166,7 +1103,7 @@ MockServer.include({
},
]);
}
const notificationData = { type: 'mark_as_read', message_ids: messageIds, needaction_inbox_counter: this._mockResPartnerGetNeedactionCount() };
const notificationData = { type: 'mark_as_read', message_ids: messageIds, needaction_inbox_counter: this._mockResPartner_GetNeedactionCount(this.currentPartnerId) };
const notification = [[false, 'res.partner', this.currentPartnerId], notificationData];
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
return messageIds;
@@ -1189,30 +1126,6 @@ MockServer.include({
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`.
*
@@ -1353,7 +1266,7 @@ MockServer.include({
),
},
]);
const data = { type: 'mark_as_read', message_ids: [message.id], needaction_inbox_counter: this._mockResPartnerGetNeedactionCount() };
const data = { type: 'mark_as_read', message_ids: [message.id], needaction_inbox_counter: this._mockResPartner_GetNeedactionCount(this.currentPartnerId) };
const busNotifications = [[[false, 'res.partner', this.currentPartnerId], data]];
this._widget.call('bus_service', 'trigger', 'notification', busNotifications);
}
@@ -1680,6 +1593,36 @@ MockServer.include({
]);
this._mockUnlink(model, [followers.map(follower => follower.id)]);
},
/**
* Simulates `_get_channels_as_member` on `res.partner`.
*
* @private
* @param {integer[]} ids
* @returns {Object}
*/
_mockResPartner_GetChannelsAsMember(ids) {
const partner = this._getRecords('res.partner', [['id', 'in', ids]])[0];
const channels = this._getRecords('mail.channel', [
['channel_type', '=', 'channel'],
['members', 'in', partner.id],
['public', 'in', ['public', 'groups']],
]);
const directMessages = this._getRecords('mail.channel', [
['channel_type', '=', 'chat'],
['is_pinned', '=', true],
['members', 'in', partner.id],
]);
const privateGroups = this._getRecords('mail.channel', [
['channel_type', '=', 'channel'],
['members', 'in', partner.id],
['public', '=', 'private'],
]);
return {
channel_channel: this._mockMailChannelChannelInfo(channels.map(channel => channel.id)),
channel_direct_message: this._mockMailChannelChannelInfo(directMessages.map(channel => channel.id)),
channel_private_group: this._mockMailChannelChannelInfo(privateGroups.map(channel => channel.id)),
};
},
/**
* Simulates `get_mention_suggestions` on `res.partner`.
*
@@ -1738,13 +1681,16 @@ MockServer.include({
return mainMatchingPartners.concat(extraMatchingPartners);
},
/**
* Simulates `get_needaction_count` on `res.partner`.
* Simulates `_get_needaction_count` on `res.partner`.
*
* @private
* @param {integer} id
* @returns {integer}
*/
_mockResPartnerGetNeedactionCount() {
_mockResPartner_GetNeedactionCount(id) {
const partner = this._getRecords('res.partner', [['id', '=', id]])[0];
return this._getRecords('mail.notification', [
['res_partner_id', '=', this.currentPartnerId],
['res_partner_id', '=', partner.id],
['is_read', '=', false],
]).length;
},
@@ -1815,4 +1761,52 @@ MockServer.include({
"name": partner.name,
};
},
/**
* Simulates `_message_fetch_failed` on `res.partner`.
*
* @private
* @param {integer} id
* @returns {Object[]}
*/
_mockResPartner_MessageFetchFailed(id) {
const partner = this._getRecords('res.partner', [['id', '=', id]])[0];
const messages = this._getRecords('mail.message', [
['author_id', '=', partner.id],
['res_id', '!=', 0],
['model', '!=', false],
['message_type', '!=', 'user_notification'],
]).filter(message => {
// Purpose is to simulate the following domain on mail.message:
// ['notification_ids.notification_status', 'in', ['bounce', 'exception']],
// But it's not supported by _getRecords domain to follow a relation.
const notifications = this._getRecords('mail.notification', [
['mail_message_id', '=', message.id],
['notification_status', 'in', ['bounce', 'exception']],
]);
return notifications.length > 0;
});
return this._mockMailMessage_MessageNotificationFormat(messages.map(message => message.id));
},
/**
* Simulates `_init_messaging` on `res.users`.
*
* @private
* @param {integer[]} ids
* @returns {Object}
*/
_mockResUsers_InitMessaging(ids) {
const user = this._getRecords('res.users', [['id', 'in', ids]])[0];
return {
channel_slots: this._mockResPartner_GetChannelsAsMember(user.partner_id),
current_partner: this._mockResPartnerMailPartnerFormat(user.partner_id),
current_user_id: this.currentUserId,
mail_failures: this._mockResPartner_MessageFetchFailed(user.partner_id),
menu_id: false, // not useful in QUnit tests
needaction_inbox_counter: this._mockResPartner_GetNeedactionCount(user.partner_id),
partner_root: this._mockResPartnerMailPartnerFormat(this.partnerRootId),
public_partners: [this._mockResPartnerMailPartnerFormat(this.publicPartnerId)],
shortcodes: this._getRecords('mail.shortcode', []),
starred_counter: this._getRecords('mail.message', [['starred_partner_ids', 'in', user.partner_id]]).length,
};
},
});
@@ -141,7 +141,7 @@ class TestDiscuss(TestMailCommon, TestRecipients):
'needaction_inbox_counter': 0}
]):
employee_partner.env['mail.message'].mark_all_as_read(domain=[])
na_count = employee_partner.get_needaction_count()
na_count = employee_partner._get_needaction_count()
self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
# mark all as read also clear inaccessible needactions
@@ -154,7 +154,7 @@ class TestDiscuss(TestMailCommon, TestRecipients):
needaction_length = len(employee_partner.env['mail.message'].search([['needaction', '=', True]]))
self.assertEqual(needaction_length, 1, "message should still be readable when notified")
na_count = employee_partner.get_needaction_count()
na_count = employee_partner._get_needaction_count()
self.assertEqual(na_count, 1, "message not accessible is currently still counted")
self._reset_bus()
@@ -166,7 +166,7 @@ class TestDiscuss(TestMailCommon, TestRecipients):
'needaction_inbox_counter': 0}
]):
employee_partner.env['mail.message'].mark_all_as_read(domain=[])
na_count = employee_partner.get_needaction_count()
na_count = employee_partner._get_needaction_count()
self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
def test_set_message_done_user(self):