diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index b3783d2f59d..5703563e95d 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -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): diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index df57a6c770e..6a568bbb269 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -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 diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 4e0a48f4325..086f02daa32 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -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} diff --git a/addons/im_livechat/tests/__init__.py b/addons/im_livechat/tests/__init__.py new file mode 100644 index 00000000000..a22a67032d1 --- /dev/null +++ b/addons/im_livechat/tests/__init__.py @@ -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 diff --git a/addons/im_livechat/tests/test_get_mail_channel.py b/addons/im_livechat/tests/test_get_mail_channel.py new file mode 100644 index 00000000000..ac3d5807333 --- /dev/null +++ b/addons/im_livechat/tests/test_get_mail_channel.py @@ -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 diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index 6cc5f093969..130e9d85478 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -123,6 +123,7 @@ + @@ -141,6 +142,7 @@ + @@ -187,10 +189,6 @@