diff options
| author | Robot Odoo <xmo+roboto@odoo.com> | 2019-04-01 09:43:42 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-04-01 09:43:42 +0200 |
| commit | 1fbafa4e69ee2cc682086a5a9d03435be88920bd (patch) | |
| tree | 8171b1769a0323875cfaa77d85c0236cfd5ba316 | |
| parent | 92e9e84b63a266b39d556c1db1d336373dc8b303 (diff) | |
| parent | 33f4980b0209ebf95970d6613d48c4ac31c83bd9 (diff) | |
[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>
| -rw-r--r-- | addons/im_livechat/controllers/main.py | 13 | ||||
| -rw-r--r-- | addons/im_livechat/models/im_livechat_channel.py | 100 | ||||
| -rw-r--r-- | addons/im_livechat/static/src/js/im_livechat.js | 26 | ||||
| -rw-r--r-- | addons/im_livechat/tests/__init__.py | 4 | ||||
| -rw-r--r-- | addons/im_livechat/tests/test_get_mail_channel.py | 60 | ||||
| -rw-r--r-- | addons/im_livechat/views/im_livechat_channel_templates.xml | 6 | ||||
| -rw-r--r-- | addons/website_livechat/models/website.py | 12 | ||||
| -rw-r--r-- | addons/website_livechat/views/website_livechat.xml | 2 |
8 files changed, 188 insertions, 35 deletions
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 () { @@ -242,6 +250,24 @@ var LivechatButton = Widget.extend({ }); }, 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 @@ <!-- 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) { diff --git a/addons/website_livechat/models/website.py b/addons/website_livechat/models/website.py index e207482416a..c52dc9a09f0 100644 --- a/addons/website_livechat/models/website.py +++ b/addons/website_livechat/models/website.py @@ -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 {} diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml index e5f429fc806..82580fc6549 100644 --- a/addons/website_livechat/views/website_livechat.xml +++ b/addons/website_livechat/views/website_livechat.xml @@ -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> |
