[MERGE][IMP] im_livechat: random assignation of conversation
Purpose The assignation method should always give the new conversation to the operator who has the less active conversation. If two operators have the same amount of active conversation, it should chose one of them randomly. We also want the visitor to get the same operator (if available) from its last visit. Specifications The method 'get_mail_channel' on the 'im_livechat.channel' model used a simple random.choice in the available users to select the operator. It was improved to select the operator that has the lowest number of open livechat sessions. If multiple operators share the same number (lowest) of open livechat sessions, it selects randomly between those. For the visitor to get the same operator as during its last visit, we save that information in a cookie (1 week lifetime), and give this optional parameter to the server when asking for the livechat session. To make the code clearer, some methods were reorganize and convert from `api.model` to `api.multi` (ensure_one) to be more API-compliant. Task-1919871 closes odoo/odoo#29888 Signed-off-by: Jérome Maes (jem) <jem@openerp.com>
This commit is contained in:
@@ -49,13 +49,12 @@ class LivechatController(http.Controller):
|
||||
def loader(self, channel_id, **kwargs):
|
||||
username = kwargs.get("username", _("Visitor"))
|
||||
channel = request.env['im_livechat.channel'].sudo().browse(channel_id)
|
||||
info = request.env['im_livechat.channel'].get_livechat_info(channel.id, username=username)
|
||||
info = channel.get_livechat_info(username=username)
|
||||
return request.render('im_livechat.loader', {'info': info, 'web_session_required': True}, headers=[('Content-Type', 'application/javascript')])
|
||||
|
||||
@http.route('/im_livechat/init', type='json', auth="public", cors="*")
|
||||
def livechat_init(self, channel_id):
|
||||
LivechatChannel = request.env['im_livechat.channel']
|
||||
available = len(LivechatChannel.browse(channel_id).get_available_users())
|
||||
available = len(request.env['im_livechat.channel'].sudo().browse(channel_id)._get_available_users())
|
||||
rule = {}
|
||||
if available:
|
||||
# find the country from the request
|
||||
@@ -81,7 +80,7 @@ class LivechatController(http.Controller):
|
||||
}
|
||||
|
||||
@http.route('/im_livechat/get_session', type="json", auth='public', cors="*")
|
||||
def get_session(self, channel_id, anonymous_name, **kwargs):
|
||||
def get_session(self, channel_id, anonymous_name, previous_operator_id=None, **kwargs):
|
||||
user_id = None
|
||||
country_id = None
|
||||
# if the user is identifiy (eg: portal user on the frontend), don't use the anonymous name. The user will be added to session.
|
||||
@@ -95,7 +94,11 @@ class LivechatController(http.Controller):
|
||||
country = request.env['res.country'].sudo().search([('code', '=', country_code)], limit=1) if country_code else None
|
||||
if country:
|
||||
anonymous_name, country_id = _("%s (%s)") % (anonymous_name, country.name), country.id
|
||||
return request.env["im_livechat.channel"].with_context(lang=False).get_mail_channel(channel_id, anonymous_name, user_id, country_id)
|
||||
|
||||
if previous_operator_id:
|
||||
previous_operator_id = int(previous_operator_id)
|
||||
|
||||
return request.env["im_livechat.channel"].with_context(lang=False).sudo().browse(channel_id)._get_mail_channel(anonymous_name, previous_operator_id, user_id, country_id)
|
||||
|
||||
@http.route('/im_livechat/feedback', type='json', auth='public', cors="*")
|
||||
def feedback(self, uuid, rate, reason=None, **kwargs):
|
||||
|
||||
@@ -124,31 +124,41 @@ class ImLivechatChannel(models.Model):
|
||||
# Channel Methods
|
||||
# --------------------------
|
||||
@api.multi
|
||||
def get_available_users(self):
|
||||
def _get_available_users(self):
|
||||
""" get available user of a given channel
|
||||
:retuns : return the res.users having their im_status online
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.sudo().user_ids.filtered(lambda user: user.im_status == 'online')
|
||||
return self.user_ids.filtered(lambda user: user.im_status == 'online')
|
||||
|
||||
@api.model
|
||||
def get_mail_channel(self, livechat_channel_id, anonymous_name, user_id=None, country_id=None):
|
||||
@api.multi
|
||||
def _get_mail_channel(self, anonymous_name, previous_operator_id=None, user_id=None, country_id=None):
|
||||
""" Return a mail.channel given a livechat channel. It creates one with a connected operator, or return false otherwise
|
||||
:param livechat_channel_id : the identifier if the im_livechat.channel
|
||||
:param anonymous_name : the name of the anonymous person of the channel
|
||||
:param previous_operator_id : partner_id.id of the previous operator that this visitor had in the past
|
||||
:param user_id : the id of the logged in visitor, if any
|
||||
:param country_code : the country of the anonymous person of the channel
|
||||
:type livechat_channel_id : int
|
||||
:type anonymous_name : str
|
||||
:return : channel header
|
||||
:rtype : dict
|
||||
|
||||
If this visitor already had an operator within the last 7 days (information stored with the 'im_livechat_previous_operator_pid' cookie),
|
||||
the system will first try to assign that operator if he's available (to improve user experience).
|
||||
"""
|
||||
# get the avalable user of the channel
|
||||
operators = self.sudo().browse(livechat_channel_id).get_available_users()
|
||||
if len(operators) == 0:
|
||||
self.ensure_one()
|
||||
|
||||
operator = False
|
||||
if previous_operator_id:
|
||||
available_users = self._get_available_users()
|
||||
# previous_operator_id is the partner_id of the previous operator, need to convert to user
|
||||
if previous_operator_id in available_users.mapped('partner_id').ids:
|
||||
operator = next(available_user for available_user in available_users if available_user.partner_id.id == previous_operator_id)
|
||||
if not operator:
|
||||
operator = self._get_random_operator()
|
||||
if not operator:
|
||||
# no one available
|
||||
return False
|
||||
# choose the res.users operator and get its partner id
|
||||
operator = random.choice(operators)
|
||||
|
||||
operator_partner_id = operator.partner_id.id
|
||||
# partner to add to the mail.channel
|
||||
channel_partner_to_add = [(4, operator_partner_id)]
|
||||
@@ -158,7 +168,7 @@ class ImLivechatChannel(models.Model):
|
||||
mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({
|
||||
'channel_partner_ids': channel_partner_to_add,
|
||||
'livechat_operator_id': operator_partner_id,
|
||||
'livechat_channel_id': livechat_channel_id,
|
||||
'livechat_channel_id': self.id,
|
||||
'anonymous_name': False if user_id else anonymous_name,
|
||||
'country_id': country_id,
|
||||
'channel_type': 'livechat',
|
||||
@@ -169,24 +179,66 @@ class ImLivechatChannel(models.Model):
|
||||
mail_channel._broadcast([operator_partner_id])
|
||||
return mail_channel.sudo().channel_info()[0]
|
||||
|
||||
@api.model
|
||||
def get_channel_infos(self, channel_id):
|
||||
channel = self.browse(channel_id)
|
||||
def _get_random_operator(self):
|
||||
""" Return a random operator from the available users of the channel that have the lowest number of active livechats.
|
||||
A livechat is considered 'active' if it has at least one message within the 30 minutes.
|
||||
|
||||
(Some annoying conversions have to be made on the fly because this model holds 'res.users' as available operators
|
||||
and the mail_channel model stores the partner_id of the randomly selected operator)
|
||||
|
||||
:return : user
|
||||
:rtype : res.users
|
||||
"""
|
||||
operators = self._get_available_users()
|
||||
if len(operators) == 0:
|
||||
return False
|
||||
|
||||
self.env.cr.execute("""SELECT COUNT(DISTINCT c.id), c.livechat_operator_id
|
||||
FROM mail_channel c
|
||||
LEFT OUTER JOIN mail_message_mail_channel_rel r ON c.id = r.mail_channel_id
|
||||
LEFT OUTER JOIN mail_message m ON r.mail_message_id = m.id
|
||||
WHERE m.create_date > ((now() at time zone 'UTC') - interval '30 minutes')
|
||||
AND c.channel_type = 'livechat'
|
||||
AND c.livechat_operator_id in %s
|
||||
GROUP BY c.livechat_operator_id
|
||||
ORDER BY COUNT(DISTINCT c.id) asc""", (tuple(operators.mapped('partner_id').ids),))
|
||||
active_channels = self.env.cr.dictfetchall()
|
||||
|
||||
# If inactive operator(s), return one of them
|
||||
active_channel_operator_ids = [active_channel['livechat_operator_id'] for active_channel in active_channels]
|
||||
inactive_operators = [operator for operator in operators if operator.partner_id.id not in active_channel_operator_ids]
|
||||
if inactive_operators:
|
||||
return random.choice(inactive_operators)
|
||||
|
||||
# If no inactive operator, active_channels is not empty as len(operators) > 0 (see above).
|
||||
# Get the less active operator using the active_channels first element's count (since they are sorted 'ascending')
|
||||
lowest_number_of_conversations = active_channels[0]['count']
|
||||
less_active_operator = random.choice([
|
||||
active_channel['livechat_operator_id'] for active_channel in active_channels
|
||||
if active_channel['count'] == lowest_number_of_conversations])
|
||||
|
||||
# convert the selected 'partner_id' to its corresponding res.users
|
||||
return next(operator for operator in operators if operator.partner_id.id == less_active_operator)
|
||||
|
||||
def _get_channel_infos(self):
|
||||
self.ensure_one()
|
||||
|
||||
return {
|
||||
'button_text': channel.button_text,
|
||||
'input_placeholder': channel.input_placeholder,
|
||||
'default_message': channel.default_message,
|
||||
"channel_name": channel.name,
|
||||
"channel_id": channel.id,
|
||||
'button_text': self.button_text,
|
||||
'input_placeholder': self.input_placeholder,
|
||||
'default_message': self.default_message,
|
||||
"channel_name": self.name,
|
||||
"channel_id": self.id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_livechat_info(self, channel_id, username='Visitor'):
|
||||
def get_livechat_info(self, username='Visitor'):
|
||||
self.ensure_one()
|
||||
|
||||
info = {}
|
||||
info['available'] = len(self.browse(channel_id).get_available_users()) > 0
|
||||
info['available'] = len(self._get_available_users()) > 0
|
||||
info['server_url'] = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
if info['available']:
|
||||
info['options'] = self.sudo().get_channel_infos(channel_id)
|
||||
info['options'] = self._get_channel_infos()
|
||||
info['options']["default_username"] = username
|
||||
return info
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ var LivechatButton = Widget.extend({
|
||||
def = session.rpc('/im_livechat/get_session', {
|
||||
channel_id : this.options.channel_id,
|
||||
anonymous_name : this.options.default_username,
|
||||
previous_operator_id: this._get_previous_operator_id(),
|
||||
}, {shadow: true});
|
||||
}
|
||||
def.then(function (livechatData) {
|
||||
@@ -233,6 +234,13 @@ var LivechatButton = Widget.extend({
|
||||
|
||||
utils.set_cookie('im_livechat_session', JSON.stringify(self._livechat.toData()), 60*60);
|
||||
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
|
||||
if (livechatData.operator_pid[0]) {
|
||||
// livechatData.operator_pid contains a tuple (id, name)
|
||||
// we are only interested in the id
|
||||
var operatorPidId = livechatData.operator_pid[0];
|
||||
var oneWeek = 7*24*60*60;
|
||||
utils.set_cookie('im_livechat_previous_operator_pid', operatorPidId, oneWeek);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).then(function () {
|
||||
@@ -241,6 +249,24 @@ var LivechatButton = Widget.extend({
|
||||
self._openingChat = false;
|
||||
});
|
||||
}, 200, true),
|
||||
/**
|
||||
* Will try to get a previous operator for this visitor.
|
||||
* If the visitor already had visitor A, it's better for his user experience
|
||||
* to get operator A again.
|
||||
*
|
||||
* The information is stored in the 'im_livechat_previous_operator_pid' cookie.
|
||||
*
|
||||
* @private
|
||||
* @return {integer} operator_id.partner_id.id if the cookie is set
|
||||
*/
|
||||
_get_previous_operator_id: function () {
|
||||
var cookie = utils.get_cookie('im_livechat_previous_operator_pid');
|
||||
if (cookie) {
|
||||
return cookie;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @return {Promise}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_get_mail_channel
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestGetMailChannel(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestGetMailChannel, self).setUp()
|
||||
self.operators = self.env['res.users'].create([{
|
||||
'name': 'Michel',
|
||||
'login': 'michel'
|
||||
}, {
|
||||
'name': 'Paul',
|
||||
'login': 'paul'
|
||||
}, {
|
||||
'name': 'Pierre',
|
||||
'login': 'pierre'
|
||||
}, {
|
||||
'name': 'Jean',
|
||||
'login': 'jean'
|
||||
}, {
|
||||
'name': 'Georges',
|
||||
'login': 'georges'
|
||||
}])
|
||||
|
||||
self.livechat_channel = self.env['im_livechat.channel'].create({
|
||||
'name': 'The channel',
|
||||
'user_ids': (6, 0, self.operators.ids)
|
||||
})
|
||||
|
||||
operators = self.operators
|
||||
def get_available_users(self):
|
||||
return operators
|
||||
|
||||
self.patch(type(self.env['im_livechat.channel']), '_get_available_users', get_available_users)
|
||||
|
||||
def test_get_mail_channel(self):
|
||||
"""For a livechat with 5 available operators, we open 5 channels 5 times (25 channels total).
|
||||
For every 5 channels opening, we check that all operators were assigned.
|
||||
"""
|
||||
|
||||
for i in range(5):
|
||||
mail_channels = self._get_mail_channels()
|
||||
channel_operators = [channel_info['operator_pid'] for channel_info in mail_channels]
|
||||
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 _get_mail_channels(self):
|
||||
mail_channels = []
|
||||
|
||||
for i in range(5):
|
||||
mail_channel = self.livechat_channel._get_mail_channel('Anonymous')
|
||||
mail_channels.append(mail_channel)
|
||||
# send a message to mark this channel as 'active'
|
||||
self.env['mail.channel'].browse(mail_channel['id']).write({
|
||||
'channel_message_ids': [(0, 0, {'body': 'cc'})]
|
||||
})
|
||||
|
||||
return mail_channels
|
||||
@@ -123,6 +123,7 @@
|
||||
<!-- Qweb2 lib -->
|
||||
<script type="text/javascript" src="/web/static/lib/qweb/qweb2.js"></script>
|
||||
<!-- Odoo JS Framework -->
|
||||
<script type="text/javascript" src="/web/static/src/js/promise_extension.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/boot.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/libs/download.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/libs/content-disposition.js"></script>
|
||||
@@ -141,6 +142,7 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/core/session.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/concurrency.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/utils.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/dom.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/qweb.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/bus.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/services/core.js"></script>
|
||||
@@ -187,10 +189,6 @@
|
||||
<!-- the js code to initialize the LiveSupport object -->
|
||||
<template id="loader" name="Livechat : Javascript appending the livechat button">
|
||||
<t t-translation="off">
|
||||
|
||||
<t t-if="not info">
|
||||
<t t-set="info" t-value="request.env['im_livechat.channel'].get_livechat_info(channel)"/>
|
||||
</t>
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
<t t-if="web_session_required">
|
||||
odoo.define('web.session', function (require) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Website(models.Model):
|
||||
@@ -9,3 +9,13 @@ class Website(models.Model):
|
||||
_inherit = "website"
|
||||
|
||||
channel_id = fields.Many2one('im_livechat.channel', string='Website Live Chat Channel')
|
||||
|
||||
@api.multi
|
||||
def get_livechat_channel_info(self):
|
||||
""" Get the livechat info dict (button text, channel name, ...) for the livechat channel of
|
||||
the current website.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.channel_id:
|
||||
return self.channel_id.sudo().get_livechat_info()
|
||||
return {}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<t t-if="website and website.channel_id">
|
||||
<script>
|
||||
<t t-call="im_livechat.loader">
|
||||
<t t-set="channel" t-value="website.channel_id.id"/>
|
||||
<t t-set="info" t-value="website.get_livechat_channel_info()"/>
|
||||
</t>
|
||||
</script>
|
||||
</t>
|
||||
|
||||
Reference in New Issue
Block a user