[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:
Robot Odoo
2019-04-01 09:43:42 +02:00
committed by GitHub
8 changed files with 188 additions and 35 deletions
+8 -5
View File
@@ -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}
+4
View File
@@ -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) {
+11 -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 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>