From 85dda405e87d3adb99c2dfa057180a1929fa7d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Mon, 30 Jun 2014 16:50:24 +0200 Subject: [PATCH] [IMP] im_chat, im_livechat : add reporting view, quit conversation, option dropdown menu, feedback for live support, better access rules and shortcode model. --- addons/im_chat/__openerp__.py | 2 + addons/im_chat/im_chat.py | 69 +++++++++- addons/im_chat/im_chat_data.xml | 87 ++++++++++++ addons/im_chat/security/ir.model.access.csv | 5 +- addons/im_chat/static/src/css/im_common.css | 19 +++ addons/im_chat/static/src/js/im_chat.js | 125 +++++++++--------- addons/im_chat/static/src/xml/im_chat.xml | 8 ++ addons/im_chat/views/im_chat_view.xml | 47 +++++++ addons/im_livechat/__init__.py | 1 + addons/im_livechat/__openerp__.py | 4 +- addons/im_livechat/im_livechat.py | 29 ++++ addons/im_livechat/im_livechat_data.xml | 47 +++++++ addons/im_livechat/report/__init__.py | 1 + .../im_livechat/report/im_livechat_report.py | 55 ++++++++ .../im_livechat/report/im_livechat_report.xml | 56 ++++++++ .../security/im_livechat_security.xml | 11 ++ .../im_livechat/security/ir.model.access.csv | 1 + .../static/src/css/im_livechat.css | 45 +++++++ addons/im_livechat/static/src/img/bad.png | Bin 0 -> 962 bytes addons/im_livechat/static/src/img/good.png | Bin 0 -> 886 bytes addons/im_livechat/static/src/img/ok.png | Bin 0 -> 739 bytes .../im_livechat/static/src/js/im_livechat.js | 60 ++++++++- .../static/src/xml/im_livechat.xml | 30 ++++- addons/im_livechat/views/im_livechat.xml | 2 + addons/im_livechat/views/im_livechat_view.xml | 95 +++++++++---- 25 files changed, 693 insertions(+), 106 deletions(-) create mode 100644 addons/im_chat/im_chat_data.xml create mode 100644 addons/im_chat/views/im_chat_view.xml create mode 100644 addons/im_livechat/im_livechat_data.xml create mode 100644 addons/im_livechat/report/__init__.py create mode 100644 addons/im_livechat/report/im_livechat_report.py create mode 100644 addons/im_livechat/report/im_livechat_report.xml create mode 100644 addons/im_livechat/static/src/css/im_livechat.css create mode 100644 addons/im_livechat/static/src/img/bad.png create mode 100644 addons/im_livechat/static/src/img/good.png create mode 100644 addons/im_livechat/static/src/img/ok.png diff --git a/addons/im_chat/__openerp__.py b/addons/im_chat/__openerp__.py index 4b1ba54c374..b8f5151165d 100644 --- a/addons/im_chat/__openerp__.py +++ b/addons/im_chat/__openerp__.py @@ -19,6 +19,8 @@ chat in real time. It support several chats in parallel. 'security/ir.model.access.csv', 'security/im_security.xml', 'views/im_chat.xml', + 'views/im_chat_view.xml', + 'im_chat_data.xml' ], 'depends' : ['base', 'web', 'bus'], 'qweb': ['static/src/xml/*.xml'], diff --git a/addons/im_chat/im_chat.py b/addons/im_chat/im_chat.py index 4be9dc89334..f865aed84f5 100644 --- a/addons/im_chat/im_chat.py +++ b/addons/im_chat/im_chat.py @@ -5,10 +5,11 @@ import logging import time import uuid import random - +import re import simplejson - import openerp +import cgi + from openerp.http import request from openerp.osv import osv, fields from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT @@ -133,6 +134,34 @@ class im_chat_session(osv.Model): user = self.pool['res.users'].read(cr, uid, user_id, ['name'], context=context) self.pool["im_chat.message"].post(cr, uid, uid, session.uuid, "meta", user['name'] + " joined the conversation.", context=context) + def remove_user(self, cr, uid, session_id, context=None): + """ private implementation of removing a user from a given session (and notify the other people) """ + session = self.browse(cr, openerp.SUPERUSER_ID, session_id, context=context) + # send a message to the conversation + user = self.pool['res.users'].read(cr, uid, uid, ['name'], context=context) + self.pool["im_chat.message"].post(cr, uid, uid, session.uuid, "meta", user['name'] + " left the conversation.", context=context) + # close his session state, and remove the user from session + self.update_state(cr, uid, session.uuid, 'closed', context=None) + self.write(cr, uid, [session.id], {"user_ids": [(3, uid)]}, context=context) + # notify the all the channel users and anonymous channel + notifications = [] + for channel_user_id in session.user_ids: + info = self.session_info(cr, channel_user_id.id, [session.id], context=context) + notifications.append([(cr.dbname, 'im_chat.session', channel_user_id.id), info]) + # anonymous are not notified when a new user left : cannot exec session_info as uid = None + info = self.session_info(cr, openerp.SUPERUSER_ID, [session.id], context=context) + notifications.append([session.uuid, info]) + self.pool['bus.bus'].sendmany(cr, uid, notifications) + + def quit_user(self, cr, uid, uuid, context=None): + """ action of leaving a given session """ + sids = self.search(cr, uid, [('uuid', '=', uuid)], context=context, limit=1) + for session in self.browse(cr, openerp.SUPERUSER_ID, sids, context=context): + if uid and uid in [u.id for u in session.user_ids] and len(session.user_ids) > 2: + self.remove_user(cr, uid, session.id, context=context) + return True + return False + def get_image(self, cr, uid, uuid, user_id, context=None): """ get the avatar of a user in the given session """ #default image @@ -165,6 +194,20 @@ class im_chat_message(osv.Model): 'type' : 'message', } + def _escape_keep_url(self, message): + """ escape the message and transform the url into clickable link """ + safe_message = "" + first = 0 + last = 0 + for m in re.finditer('(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?', message): + last = m.start() + safe_message += cgi.escape(message[first:last]) + safe_message += '%s' % (cgi.escape(m.group(0)), m.group(0)) + first = m.end() + last = m.end() + safe_message += cgi.escape(message[last:]) + return safe_message + def init_messages(self, cr, uid, context=None): """ get unread messages and old messages received less than AWAY_TIMER ago and the session_info for open or folded window @@ -210,7 +253,9 @@ class im_chat_message(osv.Model): session_ids = Session.search(cr, uid, [('uuid','=',uuid)], context=context) notifications = [] for session in Session.browse(cr, uid, session_ids, context=context): - # build the new message + # build and escape the new message + message_content = self._escape_keep_url(message_content) + message_content = self.pool['im_chat.shortcode'].replace_shortcode(cr, uid, message_content, context=context) vals = { "from_id": from_uid, "to_id": session.id, @@ -238,6 +283,24 @@ class im_chat_message(osv.Model): return False +class im_chat_shortcode(osv.Model): + """ Message shortcuts """ + _name = "im_chat.shortcode" + + _columns = { + 'source' : fields.char('Shortcut', required=True, select=True, help="The shortcut which must be replace in the Chat Messages"), + 'substitution' : fields.char('Substitution', required=True, select=True, help="The html code replacing the shortcut"), + 'description' : fields.char('Description'), + } + + def replace_shortcode(self, cr, uid, message, context=None): + ids = self.search(cr, uid, [], context=context) + for shortcode in self.browse(cr, uid, ids, context=context): + regex = "(?:^|\s)(%s)(?:\s|$)" % re.escape(shortcode.source) + message = re.sub(regex, " " + shortcode.substitution + " ", message) + return message + + class im_chat_presence(osv.Model): """ im_chat_presence status can be: online, away or offline. This model is a one2one, but is not attached to res_users to avoid database concurrence errors diff --git a/addons/im_chat/im_chat_data.xml b/addons/im_chat/im_chat_data.xml new file mode 100644 index 00000000000..ecacc1f60a3 --- /dev/null +++ b/addons/im_chat/im_chat_data.xml @@ -0,0 +1,87 @@ + + + + + + + :) + &#128522; + Smile + + + + :( + &#9785; + Sad + + + + :D + &#128517; + Laugh + + + + ;) + &#128521; + Wink + + + + :| + &#128528; + Speechless + + + + :p + &#128523; + Cheeky + + + + :s + &#128534; + Worried + + + + :/ + &#128527; + Smirking + + + + :O + &#128561; + Surprised + + + + :kitten + &#128569; + Kitten + + + + &lt;3 + &#9829; + Heart + + + + + :pinky + <img src='/im_chat/static/src/img/pinky.png'/> + Pinky + + + + :musti + <img src='/im_chat/static/src/img/musti.png'/> + Musti + + + + + diff --git a/addons/im_chat/security/ir.model.access.csv b/addons/im_chat/security/ir.model.access.csv index c1caaf14ce0..915c8a83a4d 100644 --- a/addons/im_chat/security/ir.model.access.csv +++ b/addons/im_chat/security/ir.model.access.csv @@ -3,7 +3,4 @@ access_im_chat_message,im_chat.message,model_im_chat_message,base.group_user,1,0 access_im_chat_session,im_chat.session,model_im_chat_session,base.group_user,1,1,1,0 access_im_chat_conversation_state,im_chat.conversation_state,model_im_chat_conversation_state,base.group_user,1,1,1,0 access_im_chat_presence,im_chat.presence,model_im_chat_presence,base.group_user,1,1,1,1 -access_im_chat_message_portal,im_chat.message,model_im_chat_message,base.group_portal,1,0,1,0 -access_im_chat_session_portal,im_chat.session,model_im_chat_session,base.group_portal,1,1,1,0 -access_im_chat_conversation_state_portal,im_chat.conversation_state,model_im_chat_conversation_state,base.group_portal,1,1,1,0 -access_im_chat_presence_portal,im_chat.presence,model_im_chat_presence,base.group_portal,1,1,1,1 \ No newline at end of file +access_im_chat_shortcode,im_chat.shortcode,model_im_chat_shortcode,base.group_user,1,1,1,1 diff --git a/addons/im_chat/static/src/css/im_common.css b/addons/im_chat/static/src/css/im_common.css index c89acb7d6f1..83dc9bb9c66 100644 --- a/addons/im_chat/static/src/css/im_common.css +++ b/addons/im_chat/static/src/css/im_common.css @@ -93,6 +93,25 @@ float: right; color: gray ; } + +/* options */ +.oe_im_chatview .oe_im_chatview_option_group{ + z-index: 10; +} +.oe_im_chatview .oe_im_chatview_option_list{ + right: 0; + left: auto; +} +.oe_im_chatview .oe_im_chatview_option_list li{ + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + color: #333; + white-space: nowrap; +} + .oe_im_chatview .oe_im_chatview_right div, .oe_im_chatview .oe_im_chatview_right button{ cursor: pointer; background: transparent; diff --git a/addons/im_chat/static/src/js/im_chat.js b/addons/im_chat/static/src/js/im_chat.js index 9962fdfebf9..0d27e5cbab4 100644 --- a/addons/im_chat/static/src/js/im_chat.js +++ b/addons/im_chat/static/src/js/im_chat.js @@ -165,6 +165,7 @@ className: "openerp_style oe_im_chatview", events: { "keydown input": "keydown", + "click .oe_im_chatview_options" : "click_options", "click .oe_im_chatview_close": "click_close", "click .oe_im_chatview_header": "click_header" }, @@ -207,6 +208,38 @@ self.$().show(); // prepare the header and the correct state self.update_session(); + + self.init_options(); + }, + init_options: function(){ + this.define_options(); + if(this.$('.oe_im_chatview_option_list li').length === 0){ + this.$('.oe_im_chatview_option_group').hide(); + } + }, + define_options: function(){ + // add here the option to put in the dropdown menu + this._add_option("Shortcuts", "option_shortcut", "fa fa-info-circle"); + this.$('.oe_im_chatview_option_list .option_shortcut').on('click', _.bind(this.click_option_shortcut, this)); + // quit the conversation + this._add_option('Quit discussion', 'im_chat_option_quit', 'fa fa-minus-square'); + this.$('.oe_im_chatview_option_list .im_chat_option_quit').on('click', this, _.bind(this.action_quit_conversation, this)); + + }, + _add_option: function(label, style_class, icon_fa_class){ + if(icon_fa_class){ + label = ' ' + label; + } + this.$('.oe_im_chatview_option_list').append('
  • '+label+'
  • '); + }, + action_quit_conversation: function(){ + var self = this; + var Session = new openerp.Model("im_chat.session"); + return Session.call("quit_user", [this.get("session").uuid]).then(function(res) { + if(! res){ + self.do_warn(_t("Warning"), _t("You are only 2 identified users. Just close the conversation to leave.")); + } + }); }, show: function(){ this.$().animate({ @@ -276,6 +309,7 @@ this.set("pending", this.get("pending") + 1); } this.insert_messages([message]); + this._go_bottom(); }, send_message: function(message, type) { var self = this; @@ -292,15 +326,22 @@ }, insert_messages: function(messages){ var self = this; + var get_anonymous_name = function(){ + var name = self.options["defaultUsername"]; + _.each(self.get('session').users, function(u){ + if(!u.id){ + name = u.name; + } + }); + return name; + }; // avoid duplicated messages messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; }); // escape the message content and set the timezone _.map(messages, function(m){ if(!m.from_id){ - m.from_id = [false, self.options["defaultUsername"]]; + m.from_id = [false, get_anonymous_name()]; } - m.message = self.escape_keep_url(m.message); - m.message = self.smiley(m.message); m.create_date = Date.parse(m.create_date).setTimezone("UTC").toString("yyyy-MM-dd HH:mm:ss"); return m; }); @@ -355,62 +396,6 @@ this.$("input").val(""); this.send_message(mes, "message"); }, - get_smiley_list: function(){ - var kitten = jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined; - var smileys = { - ":'(": "😢", - ":O" : "😱", - "3:)": "😈", - ":)" : "😊", - ":D" : "😅", - ";)" : "😉", - ":p" : "😋", - ":(" : "☹", - ":|" : "😐", - ":/" : "😏", - "8)" : "😳", - ":s" : "😖", - ":pinky" : "", - ":musti" : "", - }; - if(kitten){ - _.extend(smileys, { - ":)" : "😺", - ":D" : "😹", - ";)" : "😼", - ":p" : "😽", - ":(" : "🙀", - ":|" : "😿", - }); - } - return smileys; - }, - smiley: function(str){ - var re_escape = function(str){ - return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); - }; - var smileys = this.get_smiley_list(); - _.each(_.keys(smileys), function(key){ - str = str.replace( new RegExp("(?:^|\\s)(" + re_escape(key) + ")(?:\\s|$)"), ' '+smileys[key]+' '); - }); - return str; - }, - escape_keep_url: function(str){ - var url_regex = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi; - var last = 0; - var txt = ""; - while (true) { - var result = url_regex.exec(str); - if (! result) - break; - txt += _.escape(str.slice(last, result.index)); - last = url_regex.lastIndex; - var url = _.escape(result[0]); - txt += '' + url + ''; - } - txt += _.escape(str.slice(last, str.length)); - return txt; - }, _go_bottom: function() { this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight); }, @@ -420,11 +405,27 @@ focus: function() { this.$(".oe_im_chatview_input").focus(); }, - click_header: function(){ - this.update_fold_state(); + click_options: function(e){ + this.$('.oe_im_chatview_options').dropdown(); + }, + click_option_shortcut: function(){ + openerp.client.action_manager.do_action({ + type: 'ir.actions.act_window', + res_model: 'im_chat.shortcode', + view_mode: 'tree,form', + view_type: 'tree', + views: [[false, 'list'], [false, 'form']], + target: "current", + limit: 80, + }); + }, + click_header: function(event){ + var classes = event.target.className.split(' '); + if(_.contains(classes, 'oe_im_chatview_header_name') || _.contains(classes, 'oe_im_chatview_header')){ + this.update_fold_state(); + } }, click_close: function(event) { - event.stopPropagation(); this.update_fold_state('closed'); }, destroy: function() { diff --git a/addons/im_chat/static/src/xml/im_chat.xml b/addons/im_chat/static/src/xml/im_chat.xml index 0ca4fe307d2..5370635ab59 100644 --- a/addons/im_chat/static/src/xml/im_chat.xml +++ b/addons/im_chat/static/src/xml/im_chat.xml @@ -7,6 +7,14 @@ +
    + + + + +
    ×
    diff --git a/addons/im_chat/views/im_chat_view.xml b/addons/im_chat/views/im_chat_view.xml new file mode 100644 index 00000000000..e43399c4fad --- /dev/null +++ b/addons/im_chat/views/im_chat_view.xml @@ -0,0 +1,47 @@ + + + + + + Chat Shortcode + im_chat.shortcode + tree,form + +

    + Click to define a new chat shortcode. +

    + A shortcode is a keyboard shortcut. For instance, you type #gm and it will be transformed into "Good Morning". +

    +
    +
    + + + im_chat.shortcode.tree + im_chat.shortcode + + + + + + + + + + + im_chat.shortcode.form + im_chat.shortcode + +
    + + + + + + + +
    +
    +
    + +
    +
    \ No newline at end of file diff --git a/addons/im_livechat/__init__.py b/addons/im_livechat/__init__.py index e0f1b9603f9..781be8cfa8d 100644 --- a/addons/im_livechat/__init__.py +++ b/addons/im_livechat/__init__.py @@ -1 +1,2 @@ import im_livechat +import report \ No newline at end of file diff --git a/addons/im_livechat/__openerp__.py b/addons/im_livechat/__openerp__.py index 6f16c5b1872..0830333aa04 100644 --- a/addons/im_livechat/__openerp__.py +++ b/addons/im_livechat/__openerp__.py @@ -20,7 +20,9 @@ chat operators. "security/im_livechat_security.xml", "security/ir.model.access.csv", "views/im_livechat_view.xml", - "views/im_livechat.xml" + "views/im_livechat.xml", + "report/im_livechat_report.xml", + "im_livechat_data.xml" ], 'demo': [ "im_livechat_demo.xml", diff --git a/addons/im_livechat/im_livechat.py b/addons/im_livechat/im_livechat.py index 26f08761ed2..dafc2ed4c6f 100644 --- a/addons/im_livechat/im_livechat.py +++ b/addons/im_livechat/im_livechat.py @@ -23,11 +23,14 @@ import random import openerp import json import openerp.addons.im_chat.im_chat +import datetime from openerp.osv import osv, fields from openerp import tools from openerp import http from openerp.http import request +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT + class im_livechat_channel(osv.Model): _name = 'im_livechat.channel' @@ -173,6 +176,7 @@ class im_livechat_channel(osv.Model): return True class im_chat_session(osv.Model): + _inherit = 'im_chat.session' def _get_fullname(self, cr, uid, ids, fields, arg, context=None): @@ -190,8 +194,11 @@ class im_chat_session(osv.Model): _columns = { 'anonymous_name' : fields.char('Anonymous Name'), + 'create_date': fields.datetime('Create Date', required=True, select=True), 'channel_id': fields.many2one("im_livechat.channel", "Channel"), 'fullname' : fields.function(_get_fullname, type="char", string="Complete name"), + 'feedback_rating': fields.selection([('10','Good'),('5','Ok'),('1','Bad')], 'Grade', help='Feedback from user (Bad, Ok or Good)'), + 'feedback_reason': fields.text('Reason', help="Reason explaining the rating."), } def is_in_session(self, cr, uid, uuid, user_id, context=None): @@ -212,6 +219,18 @@ class im_chat_session(osv.Model): users_infos.append({'id' : False, 'name' : session.anonymous_name, 'im_status' : 'online'}) return users_infos + def quit_user(self, cr, uid, uuid, context=None): + """ action of leaving a given session """ + sids = self.search(cr, uid, [('uuid', '=', uuid)], context=context, limit=1) + for session in self.browse(cr, openerp.SUPERUSER_ID, sids, context=context): + if session.anonymous_name: + # an identified user can leave an anonymous session if there is still another idenfied user in it + if uid and uid in [u.id for u in session.user_ids] and len(session.user_ids) > 1: + self.remove_user(cr, uid, session.id, context=context) + return True + return False + else: + return super(im_chat_session, self).quit_user(cr, uid, session.id, context=context) class LiveChatController(http.Controller): @@ -249,3 +268,13 @@ class LiveChatController(http.Controller): with reg.cursor() as cr: return len(reg.get('im_livechat.channel').get_available_users(cr, uid, channel)) > 0 + + @http.route('/im_livechat/feedback', type='json', auth="none") + def feedback(self, uuid, rating, reason=None): + cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db + registry = openerp.modules.registry.RegistryManager.get(db) + Session = registry['im_chat.session'] + session_ids = Session.search(cr, uid, [('uuid','=',uuid)], context=context) + Session.write(cr, uid, session_ids, {'feedback_rating' : str(rating), 'feedback_reason' : reason}, context=context) + + diff --git a/addons/im_livechat/im_livechat_data.xml b/addons/im_livechat/im_livechat_data.xml new file mode 100644 index 00000000000..a489763a1dd --- /dev/null +++ b/addons/im_livechat/im_livechat_data.xml @@ -0,0 +1,47 @@ + + + + + + + Open Livechat Channel Menu + reload + + + + + + open + + + + + + + Sessions by day + im_livechat.report + [('nbr_speakers','>', 1)] + {'group_by': ['start_date:day', 'uuid']} + v + + + + + Last 24h stats + im_livechat.report + [('start_date','>', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') )] + {'group_by': ['start_date:day', 'uuid']} + + + + + Operators stats + im_livechat.report + [('nbr_speakers','>', 1), ('user_id', '!=', False)] + {'group_by': ['start_date:month', 'user_id', 'uuid']} + + + + + + \ No newline at end of file diff --git a/addons/im_livechat/report/__init__.py b/addons/im_livechat/report/__init__.py new file mode 100644 index 00000000000..974388ee165 --- /dev/null +++ b/addons/im_livechat/report/__init__.py @@ -0,0 +1 @@ +import im_livechat_report \ No newline at end of file diff --git a/addons/im_livechat/report/im_livechat_report.py b/addons/im_livechat/report/im_livechat_report.py new file mode 100644 index 00000000000..a0b2ce4c323 --- /dev/null +++ b/addons/im_livechat/report/im_livechat_report.py @@ -0,0 +1,55 @@ +from openerp.osv import fields,osv +from openerp import tools + + +class im_livechat_report(osv.Model): + """ Livechat Support Report """ + _name = "im_livechat.report" + _auto = False + _description = "Livechat Support Report" + _columns = { + 'uuid': fields.char('UUID', size=50, 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"), + 'rating': fields.float('Rating', readonly=True, group_operator="avg", help="Average Rating"), + '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), + } + _order = 'start_date, uuid' + + 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') + cr.execute(""" + CREATE OR REPLACE VIEW im_livechat_report AS ( + SELECT + min(M.id) as id, + S.uuid as uuid, + 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, + CAST(S.feedback_rating AS INT) as rating, + U.id as user_id, + S.channel_id as channel_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) + WHERE S.channel_id IS NOT NULL + GROUP BY U.id, M.to_id, S.id + ) + """) + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/im_livechat/report/im_livechat_report.xml b/addons/im_livechat/report/im_livechat_report.xml new file mode 100644 index 00000000000..bc8d8df8694 --- /dev/null +++ b/addons/im_livechat/report/im_livechat_report.xml @@ -0,0 +1,56 @@ + + + + + + im_livechat.report.graph + im_livechat.report + + + + + + + + + + + + im_livechat.report.search + im_livechat.report + + + + + + + + + + + + + + + + + + + + + + Livechat Support Report + im_livechat.report + form + 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. + + + + + + + + + \ No newline at end of file diff --git a/addons/im_livechat/security/im_livechat_security.xml b/addons/im_livechat/security/im_livechat_security.xml index d4cd790e058..8e000aacfbd 100644 --- a/addons/im_livechat/security/im_livechat_security.xml +++ b/addons/im_livechat/security/im_livechat_security.xml @@ -32,5 +32,16 @@ + + Live Support Managers can see all session from live support + + + [('to_id.channel_id', '!=', None)] + + + + + + diff --git a/addons/im_livechat/security/ir.model.access.csv b/addons/im_livechat/security/ir.model.access.csv index 0b8be98fd90..cb6097e549a 100644 --- a/addons/im_livechat/security/ir.model.access.csv +++ b/addons/im_livechat/security/ir.model.access.csv @@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_ls_chann1,im_livechat.channel,model_im_livechat_channel,,1,0,0,0 access_ls_chann2,im_livechat.channel,model_im_livechat_channel,group_im_livechat,1,1,1,0 access_ls_chann3,im_livechat.channel,model_im_livechat_channel,group_im_livechat_manager,1,1,1,1 +access_livechat_support_report,im_livechat.report,model_im_livechat_report,group_im_livechat_manager,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 new file mode 100644 index 00000000000..19a20483308 --- /dev/null +++ b/addons/im_livechat/static/src/css/im_livechat.css @@ -0,0 +1,45 @@ +/* feedback */ +.oe_im_chatview_feedback{ + padding: 15px; + vertical-align: middle; +} +.oe_im_chatview_feedback .oe_im_feedback_text { + color: white; + vertical-align: middle; + font-size: 14px; +} +.oe_im_chatview_feedback .oe_im_feedback_small_text { + color: white; + vertical-align: middle; + font-size: 12px; +} +.oe_im_chatview_feedback .oe_im_feedback_choices{ + height:40px; + text-align: center; + margin: 10px 0; +} +.oe_im_chatview_feedback .oe_im_feedback_choices a{ + cursor: pointer; + margin: 5px; + opacity: 0.65; +} +.oe_im_chatview_feedback .oe_im_feedback_choices a:hover{ + opacity:1; +} +.oe_im_chatview_feedback .oe_im_feedback_choices a.selected{ + opacity: 1; +} + +/* feedback reason */ +.oe_im_feedback_reason { + margin: 10px 0; +} +.oe_im_feedback_reason textarea { + width: 100%; + height: 70px; + resize: none; +} +.oe_im_feedback_btn input{ + float: right; + margin-left : 10px; +} diff --git a/addons/im_livechat/static/src/img/bad.png b/addons/im_livechat/static/src/img/bad.png new file mode 100644 index 0000000000000000000000000000000000000000..485a59bc3e5bc5f257972f804791c98ca98fecd3 GIT binary patch literal 962 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*Fy#2?0JKu0ZwbUxdh*Dr>hc;Z3)9fB;N83TJbm`GF3MwWX>5VJ+M9QuPBpYYnl}B+ zrQ45|Eql6aySJj^+tY{Mojd#C%H_{@?|gmw_}iOTzdn9gFk|K9$rI1+Jo5PT>A$~! z0j=A(XyO0=|6e`4aEak-aeD65+p}gH@U+NwSk7eV z^weNRi?Fr+Zi954YolKQhF8$@RQ9ik}HudVYX(vmbA4qt}ksXn+cIC2L@{O|Wt4f)o zjUyjyL#6ji^_fG(l>reUf$S~Ib+Mgo%33F1ZVuYRD0d}=qv3L zH$S!215r76@N~gb&bt^+DZYr9!+P7S@VOAIh#0Kl<>G}<=r*+;?e~o zr#6@t^c+uYX)~O-%6N|I@xI)%9X&i}yMyk&5IMEPJooU^2j^-VgSA$OG5x+F=@j_m zl3|4542ug^Whc%Tp4hDZ;gw9Y?hSW-gID72Q}&9#5;o{E>*FkSv3t%@pq6-JcBtV( zj*DI2l5UCal=$>f;nht382cpt#}48hBG$=a>~)WxX~y`^Shw~|ZPna+?^1j9*J$T%&BfKjc!}ENOH^O(A%94Cw{yu;-)$I z^PZ@)&ANAw*s$$>;JQ)j|4sc^-Brfx=cRrQ$@~4`ljr8IpFUR16n%Q=QlHipcD{?> zi#C7Xa<0-@&H7{9zSz01W;o~duljH^>s(&e?wM2f3!g9LlD}=|%QyRf>;4b*3EP>^ z$T0yEpK6I~L`h0wNvc(HQ7VvPFfuSS)-|xyH8Kw|G_f)@wlXr-HZZa>FjzI!PYXpu cZhlH;S|x4`w@-YV2Gqdd>FVdQ&MBb@0EcG3?*IS* literal 0 HcmV?d00001 diff --git a/addons/im_livechat/static/src/img/good.png b/addons/im_livechat/static/src/img/good.png new file mode 100644 index 0000000000000000000000000000000000000000..9ae6cce415fc5ec4e8877801098eba94bb9c9227 GIT binary patch literal 886 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*Fy#q5z)|S0MfS+t-`duW#PCE;lz( zNkK6*EO_za#h*WYsxGgwvM@b)2Q=!%yK5i6Je@pw;`iSlUp>6?|KFGY|NsB}{cFLdjYX3s&pmj) zV8+VB>9)B*rHmy(e!&b5&u*lFI7!~_E()8oZ5x0b&H|6fVg?3oVGw3ym^DX&fq_ZS z)5S5w;`H1pxzlGE2()e%XmT+KZE|4}ee>=A|D)%8(v)7`s(bNOrvKFAxhGG@EdA#% z@48ZZ!piQ$_8S&$44szSSAVcC_p8$7y$Ah#KV@$H{cuk9)ZgZr8PQzOH6YSyyLpLN&*QwMO|$b&Z;KR^G|} z4F{A&Y!}V~`u^Pv?!+X;Stnv-&aDy6o2A4ZpqBX7jb~xOCvHP+1J9!$P11ibOD&pj zcKOh+%;y~`n{pUTt|&^hac*0=Tm4;JxR~upA+frE^osVmZig#c?#52%-FnVETE+YQ z!f&p!8#f*2Th8%znzqAhE(Hs>Ek0YbLod%-S=6&kE9~0Ftm!AV%C3_(bLZY6aa9m72)F*) zJiYeE9Ny*pZj!Kd7c x7@Am_8d(`xY8x0?85lfNuZlv^kei>9nO2Eg!-T3s&wv^jJYD@<);T3K0RY7NkSPEF literal 0 HcmV?d00001 diff --git a/addons/im_livechat/static/src/img/ok.png b/addons/im_livechat/static/src/img/ok.png new file mode 100644 index 0000000000000000000000000000000000000000..b79a83240d600ec1a2fb4a465cd253699dc84a24 GIT binary patch literal 739 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*Fy#q5z)|S0MfL+0&CpPHo<}?(?Tl zyLa!YF0Zk&FjZ1eeDm(pB=l9kPQ=a{v7ow?5FY!m#_Z*xh-FdOnj<7Mq-QjGVP6 zI8ew-TiMGqQYOLF`9!DLS8cuR5`PuuOctwuQf<8`zU0r9#pl*s_CImVuWi=aHxE=6Z+|a@@<7CUN1JfqQ&1-oOQf|Dxtu6WO^v9Rj{+xF1fnAC(8QR7ZmbLwq)`(TJ`CvV01wE zoo#Je!5`$^yD}$jp7T8N6sNjJy!`DqOI!Qz_x?L1wrk-F{YN`l<&|W*4)sU=zmp-k zxyp9O?@Ata^M6_{@p{#<>jkfy)-k%QpYlU>>zcir7yJErdW%~u^Yneapu0*jXU+G| zx_SC4&#Xt&Y?qaO`2Laq#J`XgGmOuR0^?4##5JNMC9x#cD!C{XNHG{07#iyuSn3*? yhZve#8ChBxnP?jrSs57g?A6Ie(U6;;l9^VCTf^T6Q?3IwFnGH9xvX 1)){ + this.feedback = true; + this.$(".oe_im_chatview_content").html(openerp.qweb.render("support_feedback")); + this.$('.oe_im_feedback_choices a').on('click', _.bind(this.choose_feedback, this)); + this.$('.oe_im_chatview_feedback #submit').on('click', _.bind(this.submit_feedback, this)); + }else{ + if(this.rating){ + this.send_feedback(this.rating); + } + // delete cookie + openerp.set_cookie(im_livechat.COOKIE_NAME, "", -1); + this._super.apply(this, arguments); + } }, + choose_feedback: function(e){ + var self = this; + this.rating = parseInt($(e.currentTarget).data('value')); + this.$('.oe_im_feedback_choices a').removeClass('selected'); + this.$('.oe_im_feedback_choices a[data-value="'+this.rating+'"]').addClass('selected'); + }, + submit_feedback: function(e){ + var self = this; + if(this.rating){ + var reason = self.$('.oe_im_chatview_feedback textarea').val(); + if(this.rating > 1){ + this.send_feedback(this.rating, reason); + }else{ + if(reason){ + this.send_feedback(this.rating, reason); + }else{ + self.$('.oe_im_chatview_feedback textarea').css('border', 'red solid 2px'); + } + } + }else{ + this.feedback = false; + } + }, + send_feedback: function(rate, reason){ + var self = this; + openerp.session.rpc("/im_livechat/feedback", {uuid: this.get('session').uuid, rating: rate, reason : reason}).then(function(res) { + self.$(".oe_im_chatview_feedback").html($(document.createElement('div')).addClass("oe_im_feedback_text").html(_t("Thanks you for your feedback. You can close the chat window now."))); + }); + this.rating = false; + } }); // To avoid exeption when the anonymous has close his @@ -79,7 +127,7 @@ im_livechat.LiveSupport = openerp.Widget.extend({ init: function(server_url, db, channel, options) { - var self = this; + this._super(); options = options || {}; _.defaults(options, { buttonText: _t("Chat with one of our collaborators"), @@ -88,6 +136,10 @@ defaultUsername: _t("Visitor"), }); openerp.session = new openerp.Session(null, server_url, { use_cors: false }); + this.load_template(db, channel, options); + }, + load_template: function(db, channel, options){ + var self = this; // load the qweb templates var defs = []; var templates = ['/im_livechat/static/src/xml/im_livechat.xml', '/im_chat/static/src/xml/im_chat.xml']; diff --git a/addons/im_livechat/static/src/xml/im_livechat.xml b/addons/im_livechat/static/src/xml/im_livechat.xml index 1c7b00466d9..6e9d8f33520 100644 --- a/addons/im_livechat/static/src/xml/im_livechat.xml +++ b/addons/im_livechat/static/src/xml/im_livechat.xml @@ -1,7 +1,31 @@ - - - + + + + + + +
    +
    + Please take a second to rate our Livechat Support ... +
    +
    + + + +
    +
    + Click on the icon that represent the most your feeling. +
    +
    + +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/addons/im_livechat/views/im_livechat.xml b/addons/im_livechat/views/im_livechat.xml index 6ff631aab8f..dd9fdc98b6e 100644 --- a/addons/im_livechat/views/im_livechat.xml +++ b/addons/im_livechat/views/im_livechat.xml @@ -119,6 +119,8 @@ + + diff --git a/addons/im_livechat/views/im_livechat_view.xml b/addons/im_livechat/views/im_livechat_view.xml index fa18f4cab16..ee23c404f44 100644 --- a/addons/im_livechat/views/im_livechat_view.xml +++ b/addons/im_livechat/views/im_livechat_view.xml @@ -3,7 +3,8 @@ - + + Live Chat Channels im_livechat.channel kanban,form @@ -23,7 +24,7 @@ - + support_channel.kanban im_livechat.channel @@ -121,43 +122,79 @@ - + + + History - im_chat.message - list - [('to_id.channel_id', '!=', None)] - {'search_default_group_by_to_id': 1} + im_chat.session + form + list,form + [('channel_id', '!=', None)] + {'group_by': ['channel_id', 'create_date:month']} - + - - im.chat.message.tree - im_chat.message - - - - - - - - - - - - im.chat.message.search - im_chat.message + + im_chat.session.search + im_chat.session - - - + - - + + + + + + + + im_chat.session.tree + im_chat.session + + + + + + + + + + + + + im_chat.session.form + im_chat.session + +
    + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +