From 95b97d1010e2596c8dbe719382c77c6009819afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Mon, 10 Aug 2015 16:23:44 +0200 Subject: [PATCH] [REF] im_livechat : refactoring, make it compatbile with the nw mail module, change reporting sql view (split them in 2), adapt view, ... --- addons/bus/models/bus.py | 1 - addons/im_livechat/__openerp__.py | 6 +- addons/im_livechat/controllers/main.py | 40 +++---- .../data/im_livechat_channel_data.xml | 35 +----- .../im_livechat/models/im_livechat_channel.py | 58 ++++++---- addons/im_livechat/models/mail_channel.py | 102 ++++++++---------- addons/im_livechat/models/rating.py | 5 +- addons/im_livechat/report/__init__.py | 3 +- .../report/im_livechat_report_channel.py | 70 ++++++------ .../im_livechat_report_channel_views.xml | 55 +++++----- .../report/im_livechat_report_operator.py | 42 ++++++++ .../im_livechat_report_operator_views.xml | 66 ++++++++++++ .../security/im_livechat_channel_security.xml | 12 +-- .../im_livechat/security/ir.model.access.csv | 3 +- .../static/src/css/im_livechat.css | 53 +++++++-- .../im_livechat/static/src/js/im_livechat.js | 65 +++++------ .../static/src/js/im_livechat_backend.js | 53 +++++++++ .../static/src/xml/im_livechat.xml | 10 +- .../static/src/xml/im_livechat_backend.xml | 22 ++++ .../views/im_livechat_channel_templates.xml | 20 ++-- .../views/im_livechat_channel_views.xml | 10 +- .../im_livechat/views/mail_channel_views.xml | 46 ++++---- addons/mail/models/mail_channel.py | 26 ++++- .../mail/static/src/js/mail_chat_backend.js | 1 + addons/mail/static/src/js/mail_chat_common.js | 3 +- addons/mail/static/src/js/mail_thread.js | 1 + .../mail/static/src/xml/mail_chat_common.xml | 5 +- addons/mail/static/src/xml/mail_thread.xml | 14 ++- addons/mail/wizard/invite.py | 1 - addons/website_livechat/controllers/main.py | 2 +- 30 files changed, 531 insertions(+), 299 deletions(-) create mode 100644 addons/im_livechat/report/im_livechat_report_operator.py create mode 100644 addons/im_livechat/report/im_livechat_report_operator_views.xml create mode 100644 addons/im_livechat/static/src/js/im_livechat_backend.js create mode 100644 addons/im_livechat/static/src/xml/im_livechat_backend.xml diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py index 36882c5a5d8..b7d39b05ea3 100644 --- a/addons/bus/models/bus.py +++ b/addons/bus/models/bus.py @@ -53,7 +53,6 @@ class ImBus(models.Model): "message": json_dump(message) } self.sudo().create(values) - self.env.cr.commit() if random.random() < 0.01: self.gc() if channels: diff --git a/addons/im_livechat/__openerp__.py b/addons/im_livechat/__openerp__.py index 68852c27db4..da672662eca 100644 --- a/addons/im_livechat/__openerp__.py +++ b/addons/im_livechat/__openerp__.py @@ -24,13 +24,17 @@ Help your customers with this chat, and analyse their feedback. "views/mail_channel_views.xml", "views/im_livechat_channel_views.xml", "views/im_livechat_channel_templates.xml", - "report/im_livechat_report_views.xml", + "report/im_livechat_report_channel_views.xml", + "report/im_livechat_report_operator_views.xml", "data/im_livechat_channel_data.xml" ], 'demo': [ "data/im_livechat_channel_demo.xml", ], 'depends': ["mail", "rating"], + 'qweb': [ + 'static/src/xml/im_livechat_backend.xml', + ], 'installable': True, 'auto_install': False, 'application': True, diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index 68d5fbb7f29..7a4944f54ef 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -11,7 +11,8 @@ class LivechatController(http.Controller): @http.route('/im_livechat/support//', type='http', auth='none') def support_page(self, dbname, channel_id, **kwargs): Channel = request.env['im_livechat.channel'] - info = Channel.get_channel_infos(channel_id) + info = kwargs + info.update(Channel.get_channel_infos(channel_id)) info["dbname"] = dbname info["channel"] = channel_id info["channel_name"] = Channel.browse(channel_id).name @@ -39,9 +40,9 @@ class LivechatController(http.Controller): # don't return the initialization script, since its blocked (in the country) return rule_data = { - 'action' : rule.action, - 'auto_popup_timer' : rule.auto_popup_timer, - 'regex_url' : rule.regex_url, + 'action': rule.action, + 'auto_popup_timer': rule.auto_popup_timer, + 'regex_url': rule.regex_url, } info['rule'] = json.dumps(rule and rule_data or False) return request.render('im_livechat.loader', info) @@ -54,7 +55,7 @@ class LivechatController(http.Controller): # if the user is identifiy (eg: portal user on the frontend), don't use the anonymous name. The user will be added to session. if request.session.uid: anonymous_name = False - return request.env['im_livechat.channel'].sudo(request.session.uid).get_channel_session(channel_id, anonymous_name) + return request.env['im_livechat.channel'].sudo(request.session.uid or SUPERUSER_ID).get_mail_channel(channel_id, anonymous_name) @http.route('/im_livechat/available', type='json', auth="none") def available(self, db, channel): @@ -62,35 +63,34 @@ class LivechatController(http.Controller): channel = request.env['im_livechat.channel'].sudo(user_id).browse(channel) return len(channel.get_available_users()) > 0 - @http.route('/rating/livechat/feedback', type='json', auth='none') - def rating_livechat_feedback(self, uuid, rate, reason=None, **kwargs): - Session = request.env['im_chat.session'] + @http.route('/im_livechat/feedback', type='json', auth='none') + def feedback(self, uuid, rate, reason=None, **kwargs): + Channel = request.env['mail.channel'] Rating = request.env['rating.rating'] - session_ids = Session.sudo().search([('uuid', '=', uuid)]) - if session_ids: - session = session_ids[0] + channel = Channel.sudo().search([('uuid', '=', uuid)], limit=1) + print "RATING" + print channel + if channel: # limit the creation : only ONE rating per session values = { 'rating': rate, } - if not session.rating_ids: + if not channel.rating_ids: values.update({ - 'res_id': session.id, - 'res_model': 'im_chat.session', + 'res_id': channel.id, + 'res_model': 'mail.channel', 'rating': rate, 'feedback': reason, }) - # find the partner related to the user of the conversation - rated_partner_id = False - if session.user_ids: - rated_partner_id = session.user_ids[0] and session.user_ids[0].partner_id.id or False - values['rated_partner_id'] = rated_partner_id + # find the partner (operator) + if channel.channel_partner_ids: + values['rated_partner_id'] = channel.channel_partner_ids[0] and channel.channel_partner_ids[0].id or False # create the rating rating = Rating.sudo().create(values) else: if reason: values['feedback'] = reason - rating = session.rating_ids[0] + rating = channel.rating_ids[0] rating.write(values) return rating.id return False diff --git a/addons/im_livechat/data/im_livechat_channel_data.xml b/addons/im_livechat/data/im_livechat_channel_data.xml index be5c2fedf6e..2c9aa88ad2d 100644 --- a/addons/im_livechat/data/im_livechat_channel_data.xml +++ b/addons/im_livechat/data/im_livechat_channel_data.xml @@ -20,7 +20,7 @@ days -1 - im_chat.session + mail.channel cron_remove_empty_session @@ -28,47 +28,20 @@ - - - Sessions by day - im_livechat.report - [('nbr_speakers','>', 1)] - {'group_by': ['start_date:day', 'session_name']} - v - - - - - Last 24h stats - im_livechat.report - [('start_date','>', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') )] - {'group_by': ['start_date:day', 'session_name']} - - - - - Operators stats - im_livechat.report - [('nbr_speakers','>', 1), ('user_id', '!=', False)] - {'group_by': ['start_date:month', 'user_id', 'uuid']} - - - - - + :rating_0 <img src='/rating/static/src/img/rating_0.png'/> Rating Smiley Bad - + :rating_5 <img src='/rating/static/src/img/rating_5.png'/> Rating Smiley Okay - + :rating_10 <img src='/rating/static/src/img/rating_10.png'/> Rating Smiley Good diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 969d67f0909..59702b9d62d 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -24,13 +24,12 @@ class ImLivechatChannel(models.Model): def _default_user_ids(self): return [(6, 0, [self._uid])] - # attribute fields name = fields.Char('Name', required=True, help="The name of the channel") button_text = fields.Char('Text of the Button', default='Have a Question? Chat with us.', help="Default text displayed on the Livechat Support Button") default_message = fields.Char('Welcome Message', default='How may I help you?', - help="This is an automated 'welcome' message that your visitor will see when they initiate a new chat session.") + help="This is an automated 'welcome' message that your visitor will see when they initiate a new conversation.") input_placeholder = fields.Char('Chat Input Placeholder') # computed fields @@ -39,7 +38,7 @@ class ImLivechatChannel(models.Model): are_you_inside = fields.Boolean(string='Are you inside the matrix?', compute='_are_you_inside', store=False, readonly=True) script_external = fields.Text('Script (external)', compute='_compute_script_external', store=False, readonly=True) - nbr_session = fields.Integer('Number of session', compute='_compute_nbr_session', store=False, readonly=True) + nbr_channel = fields.Integer('Number of conversation', compute='_compute_nbr_channel', store=False, readonly=True) rating_percentage_satisfaction = fields.Integer('% Happy', compute='_compute_percentage_satisfaction', store=False, default=-1) # images fields @@ -56,7 +55,7 @@ class ImLivechatChannel(models.Model): # relationnal fields user_ids = fields.Many2many('res.users', 'im_livechat_channel_im_user', 'channel_id', 'user_id', string='Operators', default=_default_user_ids) - session_ids = fields.One2many('im_chat.session', 'channel_id', 'Sessions') + channel_ids = fields.One2many('mail.channel', 'livechat_channel_id', 'Sessions') rule_ids = fields.One2many('im_livechat.channel.rule', 'channel_id', 'Rules') @@ -92,16 +91,16 @@ class ImLivechatChannel(models.Model): record.web_page = "%s/im_livechat/support/%s/%i" % (base_url, self._cr.dbname, record.id) @api.multi - @api.depends('session_ids') - def _compute_nbr_session(self): + @api.depends('channel_ids') + def _compute_nbr_channel(self): for record in self: - record.nbr_session = len(record.session_ids) + record.nbr_channel = len(record.channel_ids) @api.multi - @api.depends('session_ids.rating_ids') + @api.depends('channel_ids.rating_ids') def _compute_percentage_satisfaction(self): for record in self: - repartition = record.session_ids.rating_get_grades() + repartition = record.channel_ids.rating_get_grades() total = sum(repartition.values()) if total > 0: happy = repartition['great'] @@ -109,7 +108,6 @@ class ImLivechatChannel(models.Model): else: record.rating_percentage_satisfaction = -1 - # -------------------------- # Action Methods # -------------------------- @@ -131,7 +129,7 @@ class ImLivechatChannel(models.Model): """ self.ensure_one() action = self.env['ir.actions.act_window'].for_xml_id('rating', 'action_view_rating') - action['domain'] = [('res_id', 'in', [s.id for s in self.session_ids]), ('res_model', '=', 'im_chat.session')] + action['domain'] = [('res_id', 'in', [s.id for s in self.channel_ids]), ('res_model', '=', 'mail.channel')] return action # -------------------------- @@ -146,20 +144,36 @@ class ImLivechatChannel(models.Model): return self.sudo().user_ids.filtered(lambda user: user.im_status == 'online') @api.model - def get_channel_session(self, channel_id, anonymous_name): - """ return a session given a channel : create on with a registered user, or return false otherwise """ + def get_mail_channel(self, livechat_channel_id, anonymous_name): + """ 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 + :type livechat_channel_id : int + :type anonymous_name : str + :return : channel header + :rtype : dict + """ # get the avalable user of the channel - users = self.sudo().browse(channel_id).get_available_users() + users = self.sudo().browse(livechat_channel_id).get_available_users() if len(users) == 0: return False - user_id = random.choice(users).id - # user to add to the session - user_to_add = [(4, user_id)] - if self.env.uid: - user_to_add.append((4, self.env.uid)) + # choose the res.users operator and get its partner id + user = random.choice(users) + operator_partner_id = user.partner_id.id + # partner to add to the mail.channel + channel_partner_to_add = [(4, operator_partner_id)] + if self.env.uid: # if the user if logged (portal user), he can be identify + channel_partner_to_add.append((4, self.env.user.partner_id.id)) # create the session, and add the link with the given channel - session = self.env["im_chat.session"].sudo().create({'user_ids': user_to_add, 'channel_id': channel_id, 'anonymous_name': anonymous_name}) - return session.sudo().with_context(im_livechat_operator_id=user_id).session_info() + mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({ + 'channel_partner_ids': channel_partner_to_add, + 'livechat_channel_id': livechat_channel_id, + 'anonymous_name': anonymous_name, + 'channel_type': 'livechat', + 'name': ', '.join([anonymous_name, user.name]), + 'public': 'public', + }) + return mail_channel.sudo().with_context(im_livechat_operator_partner_id=operator_partner_id).channel_info()[0] @api.model def get_channel_infos(self, channel_id): @@ -174,7 +188,6 @@ class ImLivechatChannel(models.Model): } - class ImLivechatChannelRule(models.Model): """ Channel Rules Rules defining access to the channel (countries, and url matching). It also provide the 'auto pop' @@ -202,7 +215,6 @@ class ImLivechatChannelRule(models.Model): sequence = fields.Integer('Matching order', default=10, help="Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen.") - def match_rule(self, channel_id, url, country_id=False): """ determine if a rule of the given channel match with the given url :param channel_id : the identifier of the channel_id diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py index a5af87d5580..a55e33db3d3 100644 --- a/addons/im_livechat/models/mail_channel.py +++ b/addons/im_livechat/models/mail_channel.py @@ -3,79 +3,69 @@ from openerp import api, fields, models from openerp import SUPERUSER_ID -class ImChatSession(models.Model): +class MailChannel(models.Model): """ Chat Session Reprensenting a conversation between users. It extends the base method for anonymous usage. """ - _name = 'im_chat.session' - _inherit = ['im_chat.session', 'rating.mixin'] - + _name = 'mail.channel' + _inherit = ['mail.channel', 'rating.mixin'] anonymous_name = fields.Char('Anonymous Name') create_date = fields.Datetime('Create Date', required=True) - channel_id = fields.Many2one('im_livechat.channel', 'Channel') - fullname = fields.Char('Complete Name', compute='_compute_fullname') + channel_type = fields.Selection(selection_add=[('livechat', 'Livechat Conversation')]) + livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel') @api.multi - @api.depends('anonymous_name', 'user_ids') - def _compute_fullname(self): - """ built the complete name of the session """ - for session in self: - names = [] - for user in session.user_ids: - names.append(user.name) - if session.anonymous_name: - names.append(session.anonymous_name) - session.fullname = ', '.join(names) - - @api.multi - def is_in_session(self): - """ Return True if the current user is in the user_ids of the session. False otherwise. - If this is executed as sudo, this might be the anonymous user. + def _channel_message_notifications(self, message): + """ When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when + clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header + should be added to the notification, since the user cannot be listining to the channel. """ - self.ensure_one() - if self.anonymous_name and self._uid == SUPERUSER_ID: - return True - else: - return super(ImChatSession, self).is_in_session() + notifications = super(MailChannel, self)._channel_message_notifications(message) + if not message.author_id: + unpinned_channel_partner = self.mapped('channel_last_seen_partner_ids').filtered(lambda cp: not cp.is_pinned) + if unpinned_channel_partner: + unpinned_channel_partner.write({'is_pinned': True}) + notifications = self._channel_channel_notifications(unpinned_channel_partner.mapped('partner_id').ids) + notifications + return notifications @api.multi - def session_user_info(self): - """ Get the user infos for all the identified user in the session + the anonymous if anonymous session - :returns a list of user infos + def channel_info(self): + """ Extends the channel header by adding the livechat operator and the 'anonymous' profile :rtype : list(dict) """ - self.ensure_one() - users_infos = super(ImChatSession, self).session_user_info() - # identify the operator for the 'welcome message' - for user_profile in users_infos: - user_profile['is_operator'] = bool(user_profile['id'] == self.env.context.get('im_livechat_operator_id')) - if self.anonymous_name: - users_infos.append({'id': False, 'name': self.anonymous_name, 'im_status': 'online', 'is_operator': False}) - return users_infos + channel_infos = super(MailChannel, self).channel_info() + # add the operator id + if self.env.context.get('im_livechat_operator_partner_id'): + partner_name = self.env['res.partner'].browse(self.env.context.get('im_livechat_operator_partner_id')).name_get()[0] + for channel_info in channel_infos: + channel_info['operator_pid'] = partner_name + # add the anonymous name + channel_infos_dict = dict((c['id'], c) for c in channel_infos) + for channel in self: + if channel.anonymous_name: + channel_infos_dict[channel.id]['anonymous_name'] = channel.anonymous_name + return channel_infos_dict.values() @api.model - def quit_user(self, uuid): - """ Remove the current user from the given session. - Note : an anonymous user cannot leave the session, since he is not registered. - Required to modify the base comportement, since a session can contain only 1 identified user. - :param uuid : the uuid of the session to quit - """ - session = self.search([('uuid', '=', uuid)], limit=1) - if session.anonymous_name: - # an identified user can leave an anonymous session if there is still another idenfied user in it - if self._uid and self._uid in [u.id for u in session.user_ids] and len(session.user_ids) > 1: - self._remove_user() - return True - return False - else: - return super(ImChatSession, self).quit_user(uuid) + def channel_fetch_slot(self): + values = super(MailChannel, self).channel_fetch_slot() + pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('is_pinned', '=', True)]).mapped('channel_id') + values['channel_livechat'] = self.search([('channel_type', '=', 'livechat'), ('public', 'in', ['public']), ('id', 'in', pinned_channels.ids)]).channel_info() + return values @api.model def cron_remove_empty_session(self): - groups = self.env['im_chat.message'].read_group([], ['to_id'], ['to_id']) - not_empty_session_ids = [group['to_id'][0] for group in groups] - empty_sessions = self.search([('id', 'not in', not_empty_session_ids), ('channel_id', '!=', False)]) - empty_sessions.unlink() + self.env.cr.execute(""" + SELECT id as id + FROM mail_channel C + WHERE NOT EXISTS ( + SELECT * + FROM mail_message_mail_channel_rel R + WHERE R.mail_channel_id = C.id + ) AND C.channel_type = 'livechat' AND livechat_channel_id IS NOT NULL; + """) + empty_channel_ids = [item['id'] for item in self.env.cr.dictfetchall()] + self.browse(empty_channel_ids).unlink() diff --git a/addons/im_livechat/models/rating.py b/addons/im_livechat/models/rating.py index a7d1cf0531c..c0271a0bb34 100644 --- a/addons/im_livechat/models/rating.py +++ b/addons/im_livechat/models/rating.py @@ -11,9 +11,8 @@ class Rating(models.Model): def _compute_res_name(self): # cannot change the rec_name of session since it is use to create the bus channel # so, need to override this method to set the same alternative rec_name as in reporting - if self.res_model == 'im_chat.session': + if self.res_model == 'mail.channel': current_object = self.env[self.res_model].sudo().browse(self.res_id) - self.res_name = ('%s / %s') % (current_object.channel_id.name, current_object.id) + self.res_name = ('%s / %s') % (current_object.livechat_channel_id.name, current_object.id) else: super(Rating, self)._compute_res_name() - diff --git a/addons/im_livechat/report/__init__.py b/addons/im_livechat/report/__init__.py index edfc5d9deeb..02fe2345c7c 100644 --- a/addons/im_livechat/report/__init__.py +++ b/addons/im_livechat/report/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- -import im_livechat_report +import im_livechat_report_channel +import im_livechat_report_operator diff --git a/addons/im_livechat/report/im_livechat_report_channel.py b/addons/im_livechat/report/im_livechat_report_channel.py index 4fa0436b001..5e5c012753e 100644 --- a/addons/im_livechat/report/im_livechat_report_channel.py +++ b/addons/im_livechat/report/im_livechat_report_channel.py @@ -1,55 +1,51 @@ # -*- coding: utf-8 -*- -from openerp import api, fields, models, tools +from openerp import fields, models, tools -class im_livechat_report(models.Model): - """ Livechat Support Report """ +class ImLivechatReportChannel(models.Model): + """ Livechat Support Report on the Channels """ - _name = "im_livechat.report" + _name = "im_livechat.report.channel" _description = "Livechat Support Report" - _order = 'start_date, session_name' + _order = 'start_date, technical_name' _auto = False - session_name = fields.Char('Session Name', readonly=True) uuid = fields.Char('UUID', readonly=True) + channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True) + channel_name = fields.Char('Channel Name', readonly=True) + technical_name = fields.Char('Code', readonly=True) + livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True) start_date = fields.Datetime('Start Date of session', readonly=True, help="Start date of the conversation") start_date_hour = fields.Char('Hour of start Date of session', readonly=True) duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)") - time_in_session = fields.Float('Time in session', digits=(16, 2), readonly=True, group_operator="avg", help="Average time the user spend in the conversation") - time_to_answer = fields.Float('Time to answer', digits=(16, 2), readonly=True, group_operator="avg", help="Average time to give the first answer to the visitor") - nbr_messages = fields.Integer('Average message', readonly=True, group_operator="avg", help="Number of message in the conversation") - nbr_user_messages = fields.Integer('Average of messages/user', readonly=True, group_operator="avg", help="Average number of message per user") - nbr_speakers = fields.Integer('# of speakers', readonly=True, group_operator="avg", help="Number of different speakers") - user_id = fields.Many2one('res.users', 'User', readonly=True) - session_id = fields.Many2one('im_chat.session', 'Session', readonly=True) - channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True) - + nbr_speaker = fields.Integer('# of speakers', readonly=True, group_operator="avg", help="Number of different speakers") + nbr_message = fields.Integer('Average message', readonly=True, group_operator="avg", help="Number of message in the conversation") + partner_id = fields.Many2one('res.partner', 'Opertor', readonly=True) def init(self, cr): # Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view ! - tools.drop_view_if_exists(cr, 'im_livechat_report') + tools.drop_view_if_exists(cr, 'im_livechat_report_channel') cr.execute(""" - CREATE OR REPLACE VIEW im_livechat_report AS ( + CREATE OR REPLACE VIEW im_livechat_report_channel AS ( SELECT - min(M.id) as id, - S.uuid as uuid, - CONCAT(C.name, ' / ', S.id) as session_name, - S.create_date as start_date, - to_char(date_trunc('hour', S.create_date), 'YYYY-MM-DD HH24:MI:SS') as start_date_hour, - EXTRACT('epoch' from ((SELECT (max(create_date)-min(create_date)) FROM im_chat_message WHERE to_id=S.id AND from_id = U.id))) as time_in_session, - EXTRACT('epoch' from ((SELECT min(create_date) FROM im_chat_message WHERE to_id=S.id AND from_id IS NOT NULL)-(SELECT min(create_date) FROM im_chat_message WHERE to_id=S.id AND from_id IS NULL))) as time_to_answer, - EXTRACT('epoch' from (max((SELECT (max(create_date)) FROM im_chat_message WHERE to_id=S.id))-S.create_date)) as duration, - (SELECT count(distinct COALESCE(from_id, 0)) FROM im_chat_message WHERE to_id=S.id) as nbr_speakers, - (SELECT count(id) FROM im_chat_message WHERE to_id=S.id) as nbr_messages, - count(M.id) as nbr_user_messages, - U.id as user_id, - S.channel_id as channel_id, - S.id as session_id - FROM im_chat_message M - LEFT JOIN im_chat_session S on (S.id = M.to_id) - LEFT JOIN res_users U on (U.id = M.from_id) - LEFT JOIN im_livechat_channel C on (S.channel_id = C.id) - WHERE S.channel_id IS NOT NULL - GROUP BY U.id, M.to_id, S.id, C.name, S.uuid + C.id as id, + C.uuid as uuid, + C.id as channel_id, + C.name as channel_name, + CONCAT(L.name, ' / ', C.id) as technical_name, + C.livechat_channel_id as livechat_channel_id, + C.create_date as start_date, + to_char(date_trunc('hour', C.create_date), 'YYYY-MM-DD HH24:MI:SS') as start_date_hour, + EXTRACT('epoch' FROM (max((SELECT (max(M.create_date)) FROM mail_message M JOIN mail_message_mail_channel_rel R ON (R.mail_message_id = M.id) WHERE R.mail_channel_id = C.id))-C.create_date)) as duration, + count(distinct P.id) as nbr_speaker, + count(distinct M.id) as nbr_message, + MAX(S.partner_id) as partner_id + FROM mail_channel C + JOIN mail_message_mail_channel_rel R ON (C.id = R.mail_channel_id) + JOIN mail_message M ON (M.id = R.mail_message_id) + JOIN mail_channel_partner S ON (S.channel_id = C.id) + JOIN im_livechat_channel L ON (L.id = C.livechat_channel_id) + LEFT JOIN res_partner P ON (M.author_id = P.id) + GROUP BY C.id, C.name, C.livechat_channel_id, L.name, C.create_date, C.uuid ) """) diff --git a/addons/im_livechat/report/im_livechat_report_channel_views.xml b/addons/im_livechat/report/im_livechat_report_channel_views.xml index 65ad87120dd..d8d9ed7dcfa 100644 --- a/addons/im_livechat/report/im_livechat_report_channel_views.xml +++ b/addons/im_livechat/report/im_livechat_report_channel_views.xml @@ -2,42 +2,42 @@ - - im_livechat.report.pivot - im_livechat.report + + im_livechat.report.channel.pivot + im_livechat.report.channel - + - - + - - im_livechat.report.graph - im_livechat.report + + im_livechat.report.channel.graph + im_livechat.report.channel - - + + - - im_livechat.report.search - im_livechat.report + + im_livechat.report.channel.search + im_livechat.report.channel - - + + + - + @@ -49,22 +49,29 @@ - - Livechat Support Report - im_livechat.report + + Livechat Support Report Channel + im_livechat.report.channel form pivot,graph - Livechat Support Analysis allows you to easily check and analyse your company livechat session performance. Extract informations about the missed sessions, the audiance, the duration of a session, etc. + {"search_default_last_week":1} + Livechat Support Channel Statistics allows you to easily check and analyse your company livechat session performance. Extract informations about the missed sessions, the audiance, the duration of a session, etc. - + groups="im_livechat_group_manager"/> + + + diff --git a/addons/im_livechat/report/im_livechat_report_operator.py b/addons/im_livechat/report/im_livechat_report_operator.py new file mode 100644 index 00000000000..7a15c90ef11 --- /dev/null +++ b/addons/im_livechat/report/im_livechat_report_operator.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from openerp import fields, models, tools + + +class ImLivechatReportOperator(models.Model): + """ Livechat Support Report on the Operator """ + + _name = "im_livechat.report.operator" + _description = "Livechat Support Report" + _order = 'livechat_channel_id, partner_id' + _auto = False + + partner_id = fields.Many2one('res.partner', 'Operator', readonly=True) + livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True) + nbr_channel = fields.Integer('# of channel', readonly=True, group_operator="sum", help="Number of conversation") + channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True) + start_date = fields.Datetime('Start Date of session', readonly=True, help="Start date of the conversation") + time_to_answer = fields.Float('Time to answer', digits=(16, 2), readonly=True, group_operator="avg", help="Average time to give the first answer to the visitor") + duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)") + + def init(self, cr): + # Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view ! + tools.drop_view_if_exists(cr, 'im_livechat_report_operator') + cr.execute(""" + CREATE OR REPLACE VIEW im_livechat_report_operator AS ( + SELECT + row_number() OVER () AS id, + P.id as partner_id, + L.id as livechat_channel_id, + count(C.id) as nbr_channel, + C.id as channel_id, + C.create_date as start_date, + EXTRACT('epoch' FROM (max((SELECT (max(M.create_date)) FROM mail_message M JOIN mail_message_mail_channel_rel R ON (R.mail_message_id = M.id) WHERE R.mail_channel_id = C.id))-C.create_date)) as duration, + EXTRACT('epoch' from ((SELECT min(M.create_date) FROM mail_message M, mail_message_mail_channel_rel R WHERE M.author_id=P.id AND R.mail_channel_id = C.id AND R.mail_message_id = M.id)-(SELECT min(M.create_date) FROM mail_message M, mail_message_mail_channel_rel R WHERE M.author_id IS NULL AND R.mail_channel_id = C.id AND R.mail_message_id = M.id))) as time_to_answer + FROM im_livechat_channel_im_user O + JOIN res_users U ON (O.user_id = U.id) + JOIN res_partner P ON (U.partner_id = P.id) + LEFT JOIN im_livechat_channel L ON (L.id = O.channel_id) + LEFT JOIN mail_channel C ON (C.livechat_channel_id = L.id) + GROUP BY P.id, L.id, C.id, C.create_date + ) + """) diff --git a/addons/im_livechat/report/im_livechat_report_operator_views.xml b/addons/im_livechat/report/im_livechat_report_operator_views.xml new file mode 100644 index 00000000000..902a16ab1ac --- /dev/null +++ b/addons/im_livechat/report/im_livechat_report_operator_views.xml @@ -0,0 +1,66 @@ + + + + + + im_livechat.report.operator.pivot + im_livechat.report.operator + + + + + + + + + + + im_livechat.report.operator.graph + im_livechat.report.operator + + + + + + + + + + im_livechat.report.operator.search + im_livechat.report.operator + + + + + + + + + + + + + + + + + + + + Livechat Support Report Channel + im_livechat.report.operator + form + pivot,graph + {"search_default_last_week":1} + Livechat Support Channel Statistics allows you to easily check and analyse your company livechat session performance. Extract informations about the missed sessions, the audiance, the duration of a session, etc. + + + + + + diff --git a/addons/im_livechat/security/im_livechat_channel_security.xml b/addons/im_livechat/security/im_livechat_channel_security.xml index 71c8b08739a..5fa0888b950 100644 --- a/addons/im_livechat/security/im_livechat_channel_security.xml +++ b/addons/im_livechat/security/im_livechat_channel_security.xml @@ -21,22 +21,22 @@ - + Live Support Managers can read messages from live support - + - [('to_id.channel_id', '!=', None)] + [('mail.channel_ids.livechat_channel_id', '!=', None)] - + Live Support Managers can see all session from live support - + - [('channel_id', '!=', None)] + [('mail.channel_ids.livechat_channel_id', '!=', None)] diff --git a/addons/im_livechat/security/ir.model.access.csv b/addons/im_livechat/security/ir.model.access.csv index f6cc4ed628d..7f0681c1673 100644 --- a/addons/im_livechat/security/ir.model.access.csv +++ b/addons/im_livechat/security/ir.model.access.csv @@ -2,7 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_livechat_channel,im_livechat.channel,model_im_livechat_channel,,1,0,0,0 access_livechat_channel_user,im_livechat.channel,model_im_livechat_channel,im_livechat_group_user,1,1,1,0 access_livechat_channel_manager,im_livechat.channel,model_im_livechat_channel,im_livechat_group_manager,1,1,1,1 -access_livechat_support_report,im_livechat.report,model_im_livechat_report,im_livechat_group_manager,1,1,1,1 +access_livechat_support_report_channel,im_livechat.report.channel,model_im_livechat_report_channel,im_livechat_group_manager,1,1,1,1 +access_livechat_support_report_operator,im_livechat.report.operator,model_im_livechat_report_operator,im_livechat_group_manager,1,1,1,1 access_livechat_channel_rule,im_livechat.channel.rule,model_im_livechat_channel_rule,,1,0,0,0 access_livechat_channel_rule_user,im_livechat.channel.rule,model_im_livechat_channel_rule,im_livechat_group_user,1,1,1,0 access_livechat_channel_rule_manager,im_livechat.channel.rule,model_im_livechat_channel_rule,im_livechat_group_user,1,1,1,1 diff --git a/addons/im_livechat/static/src/css/im_livechat.css b/addons/im_livechat/static/src/css/im_livechat.css index becceb34f7f..07040610e99 100644 --- a/addons/im_livechat/static/src/css/im_livechat.css +++ b/addons/im_livechat/static/src/css/im_livechat.css @@ -1,42 +1,75 @@ + +.openerp { /* base style of openerp */ + font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; + color: #4c4c4c; + font-size: 13px; +} + +.o_mail_chat_im_window * { + box-sizing: border-box; +} + + +.oe_chat_button { + width: 230px; + position: fixed; + bottom: 0px; + right: 6px; + display: inline-block; + min-width: 100px; + background-color: rgba(60, 60, 60, 0.6); + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; + font-size: 14px; + font-weight: bold; + padding: 10px; + color: white; + text-shadow: rgb(59, 76, 88) 1px 1px 0px; + border: 1px solid rgb(80, 80, 80); + border-bottom: 0px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + cursor: pointer; + z-index:500; +} + /* Livechat Rating : feedback smiley */ -.oe_im_chatview .oe_livechat_rating{ +.o_mail_chat_im_window_content .o_livechat_rating{ padding: 15px; vertical-align: middle; } -.oe_im_chatview .oe_livechat_rating .oe_im_feedback_text { - color: white; +.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_feedback_text { vertical-align: middle; font-size: 14px; } -.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices{ +.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_choices{ height:40px; text-align: center; margin: 10px 0 30px 0; } -.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img{ +.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_choices img{ width: 50px; opacity: 0.60; cursor: pointer; margin: 5px; } -.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img:hover{ +.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_choices img:hover{ opacity:1; } -.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img.selected{ +.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_choices img.selected{ opacity: 1; } /* feedback reason */ -.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason { +.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_reason { margin: 10px 0; display: none; /* hidden by default */ } -.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason textarea { +.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_reason textarea { width: 100%; height: 70px; resize: none; } -.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason_button input{ +.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_reason_button input{ float: right; margin-left : 10px; } diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index d59f7b4a5cf..faabae44909 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -7,7 +7,7 @@ var user_session = require('web.session'); var time = require('web.time'); var utils = require('web.utils'); var Widget = require('web.Widget'); -var im_chat_common = require('mail.chat_common'); +var mail_chat_common = require('mail.chat_common'); var _t = core._t; var QWeb = core.qweb; @@ -19,11 +19,12 @@ var COOKIE_NAME = 'livechat_conversation'; The state of anonymous session is hold by the client and not the server. Override the method managing the state of normal conversation. */ -im_chat_common.Conversation.include({ - init: function(){ +mail_chat_common.Conversation.include({ + init: function(parent, session, options){ this._super.apply(this, arguments); this.shown = true; this.feedback = false; + this.options.default_username = session.anonymous_name || this.options.default_username; }, show: function(){ this._super.apply(this, arguments); @@ -33,7 +34,7 @@ im_chat_common.Conversation.include({ this._super.apply(this, arguments); this.shown = false; }, - session_update_state: function(state){ + session_fold: function(state){ // set manually the new state if(state === 'closed'){ this.destroy(); @@ -54,14 +55,14 @@ im_chat_common.Conversation.include({ this._super(state); utils.set_cookie(COOKIE_NAME, JSON.stringify(this.get('session')), 60*60); }, - click_close: function(event) { + on_click_close: function(event) { if(!this.feedback && (this.get('messages').length > 1)){ this.feedback = new Feedback(this); - this.$(".oe_im_chatview_content").empty(); - this.$(".oe_im_chatview_input").prop('disabled', true); - this.feedback.appendTo( this.$(".oe_im_chatview_content")); + this.$(".o_mail_chat_im_window_content").empty(); + this.$(".o_mail_chat_im_window_input").prop('disabled', true); + this.feedback.appendTo(this.$(".o_mail_chat_im_window_content")); // bind event to close conversation - this.feedback.on("feedback_sent", this, this.click_close); + this.feedback.on("feedback_sent", this, this.on_click_close); }else{ this._super.apply(this, arguments); utils.set_cookie(COOKIE_NAME, "", -1); @@ -72,7 +73,7 @@ im_chat_common.Conversation.include({ // To avoid exeption when the anonymous has close his // conversation and when operator send him a message. -im_chat_common.ConversationManager.include({ +var LivechatConversationManager = mail_chat_common.ConversationManager.extend({ message_receive: function(message) { try{ this._super(message); @@ -95,7 +96,7 @@ var LiveSupport = Widget.extend({ this.load_template(db, channel, options, rule); }, _get_template_list: function(){ - return ['/im_livechat/static/src/xml/im_livechat.xml', '/im_chat/static/src/xml/im_chat.xml']; + return ['/im_livechat/static/src/xml/im_livechat.xml', '/mail/static/src/xml/mail_chat_common.xml']; }, load_template: function(db, channel, options, rule){ var self = this; @@ -153,7 +154,7 @@ var ChatButton = Widget.extend({ start: function() { this.$().append(QWeb.render("im_livechat.chatButton", {widget: this})); // set up the manager - this.manager = new im_chat_common.ConversationManager(this, this.options); + this.manager = new LivechatConversationManager(this, this.options); this.manager.set("bottom_offset", $('.oe_chat_button').outerHeight()); if(this.session){ this.set_conversation(this.session); @@ -200,20 +201,20 @@ var ChatButton = Widget.extend({ }, send_welcome_message: function(){ var self = this; - if(this.session.users.length > 0){ - if (self.options.defaultMessage) { - setTimeout(function(){ - var operator = _.last(_.filter(self.session.users, function(user){return user.is_operator})); - self.conv.message_receive({ - id : 1, - type: "message", - message: self.options.defaultMessage, - create_date: time.datetime_to_str(new Date()), - from_id: [operator.id, operator.name], - to_id: [0, self.session.uuid] - }); - }, 1000); - } + if(this.session.operator_pid && self.options.defaultMessage) { + setTimeout(function(){ + self.conv.message_receive({ + id : 1, + message_type: "comment", + model: 'mail.channel', + body: self.options.defaultMessage, + date: time.datetime_to_str(new Date()), + author_id: self.session.operator_pid, + channel_ids: [self.session.id], + tracking_value_ids: [], + attachment_ids: [], + }); + }, 1000); } } }); @@ -231,20 +232,20 @@ var Feedback = Widget.extend({ start: function(){ this._super.apply(this.arguments); // bind events - this.$('.oe_livechat_rating_choices img').on('click', _.bind(this.click_smiley, this)); + this.$('.o_livechat_rating_choices img').on('click', _.bind(this.click_smiley, this)); this.$('#rating_submit').on('click', _.bind(this.click_send, this)); }, click_smiley: function(ev){ var self = this; this.rating = parseInt($(ev.currentTarget).data('value')); - this.$('.oe_livechat_rating_choices img').removeClass('selected'); - this.$('.oe_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected'); + this.$('.o_livechat_rating_choices img').removeClass('selected'); + this.$('.o_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected'); // only display textearea if bad smiley selected var close_conv = false; if(this.rating === 0){ - this.$('.oe_livechat_rating_reason').show(); + this.$('.o_livechat_rating_reason').show(); }else{ - this.$('.oe_livechat_rating_reason').hide(); + this.$('.o_livechat_rating_reason').hide(); close_conv = true; } this._send_feedback(close_conv).then(function(){ @@ -260,7 +261,7 @@ var Feedback = Widget.extend({ _send_feedback: function(close){ var self = this; var uuid = this.conversation.get('session').uuid; - return user_session.rpc('/rating/livechat/feedback', {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) { + return user_session.rpc('/im_livechat/feedback', {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) { if(close){ self.trigger("feedback_sent"); // will close the conversation self.conversation.message_send(_.str.sprintf(_t("I rated you with :rating_%d"), self.rating), "message"); diff --git a/addons/im_livechat/static/src/js/im_livechat_backend.js b/addons/im_livechat/static/src/js/im_livechat_backend.js new file mode 100644 index 00000000000..e3a03db1248 --- /dev/null +++ b/addons/im_livechat/static/src/js/im_livechat_backend.js @@ -0,0 +1,53 @@ +odoo.define('im_livechat.chat_backend', function (require) { +"use strict"; + +var mail_chat_backend = require('mail.chat_backend'); + + +/** + * Patch for the client action to integrate Livechat Mail Channel in a particular slot + */ +mail_chat_backend.ChatMailThread.include({ + init: function(parent, action){ + this._super.apply(this, arguments); + this.set('channel_livechat', []); + }, + start: function(){ + var self = this; + this.on("change:channel_livechat", this, function(){ + self.channel_render('channel_livechat'); + }); + return this._super.apply(this, arguments).then(function(){ + self.$el.on('click', '.o_mail_chat_channel_unpin', _.bind(self.on_click_channel_unpin, self)); + }); + }, + on_click_channel_unpin: function(event){ + var self = this; + var channel_id = this.$(event.currentTarget).data('channel-id'); + var channel = this.channels[channel_id]; + return this.channel_pin(channel.uuid, false).then(function(){ + self.channel_remove(channel_id); + // if unpin current channel, switch to inbox + if(self.get('current_channel_id') === channel_id){ + self.set('current_channel_id', 'channel_inbox'); + } + }); + }, + channel_change: function(){ + // update the default username + var current_channel = this.channels[this.get('current_channel_id')]; + if(current_channel){ + this.options.default_username = current_channel.anonymous_name || this.options.default_username + } + return this._super.apply(this, arguments); + }, + // utils function + get_channel_slot: function(channel){ + if(channel.channel_type === 'livechat'){ + return 'channel_livechat'; + } + return this._super.apply(this, arguments); + }, +}); + +}); diff --git a/addons/im_livechat/static/src/xml/im_livechat.xml b/addons/im_livechat/static/src/xml/im_livechat.xml index fb0b9ca3072..fc004cb4dcf 100644 --- a/addons/im_livechat/static/src/xml/im_livechat.xml +++ b/addons/im_livechat/static/src/xml/im_livechat.xml @@ -7,18 +7,18 @@ -
-
+
+
Did we correctly answer your question ?
-
+
Good OK Bad
-
+
-
+
diff --git a/addons/im_livechat/static/src/xml/im_livechat_backend.xml b/addons/im_livechat/static/src/xml/im_livechat_backend.xml new file mode 100644 index 00000000000..a48027b6047 --- /dev/null +++ b/addons/im_livechat/static/src/xml/im_livechat_backend.xml @@ -0,0 +1,22 @@ + + diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index 767d3986e84..c4a3aa3351a 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -2,6 +2,15 @@ + + + diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml index 06f8e3ec6de..9a49b91580a 100644 --- a/addons/im_livechat/views/im_livechat_channel_views.xml +++ b/addons/im_livechat/views/im_livechat_channel_views.xml @@ -29,7 +29,7 @@ - +
@@ -44,7 +44,7 @@ Operators
- Sessions + Sessions

@@ -66,8 +66,8 @@
-