[IMP] im_chat, im_livechat : add reporting view, quit conversation, option dropdown menu, feedback for live support, better access rules and shortcode model.

This commit is contained in:
Jérome Maes
2014-10-03 12:08:02 +02:00
parent 7c7c2c72ee
commit 85dda405e8
25 changed files with 693 additions and 106 deletions
+2
View File
@@ -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'],
+66 -3
View File
@@ -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 += '<a href="%s" target="_blank">%s</a>' % (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
+87
View File
@@ -0,0 +1,87 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Smiley (html code) -->
<record id="im_chat_smiley_smile" model="im_chat.shortcode">
<field name="source">:)</field>
<field name="substitution">&amp;#128522;</field>
<field name="description">Smile</field>
</record>
<record id="im_chat_smiley_sad" model="im_chat.shortcode">
<field name="source">:(</field>
<field name="substitution">&amp;#9785;</field>
<field name="description">Sad</field>
</record>
<record id="im_chat_smiley_laugh" model="im_chat.shortcode">
<field name="source">:D</field>
<field name="substitution">&amp;#128517;</field>
<field name="description">Laugh</field>
</record>
<record id="im_chat_smiley_wink" model="im_chat.shortcode">
<field name="source">;)</field>
<field name="substitution">&amp;#128521;</field>
<field name="description">Wink</field>
</record>
<record id="im_chat_smiley_speechless" model="im_chat.shortcode">
<field name="source">:|</field>
<field name="substitution">&amp;#128528;</field>
<field name="description">Speechless</field>
</record>
<record id="im_chat_smiley_cheeky" model="im_chat.shortcode">
<field name="source">:p</field>
<field name="substitution">&amp;#128523;</field>
<field name="description">Cheeky</field>
</record>
<record id="im_chat_smiley_worried" model="im_chat.shortcode">
<field name="source">:s</field>
<field name="substitution">&amp;#128534;</field>
<field name="description">Worried</field>
</record>
<record id="im_chat_smiley_smirking" model="im_chat.shortcode">
<field name="source">:/</field>
<field name="substitution">&amp;#128527;</field>
<field name="description">Smirking</field>
</record>
<record id="im_chat_smiley_surprised" model="im_chat.shortcode">
<field name="source">:O</field>
<field name="substitution">&amp;#128561;</field>
<field name="description">Surprised</field>
</record>
<record id="im_chat_smiley_kitten" model="im_chat.shortcode">
<field name="source">:kitten</field>
<field name="substitution">&amp;#128569;</field>
<field name="description">Kitten</field>
</record>
<record id="im_chat_smiley_heart" model="im_chat.shortcode">
<field name="source">&amp;lt;3</field>
<field name="substitution">&amp;#9829;</field>
<field name="description">Heart</field>
</record>
<!-- Smiley image -->
<record id="im_chat_smiley_pinky" model="im_chat.shortcode">
<field name="source">:pinky</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/pinky.png'/&gt;</field>
<field name="description">Pinky</field>
</record>
<record id="im_chat_smiley_musti" model="im_chat.shortcode">
<field name="source">:musti</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/musti.png'/&gt;</field>
<field name="description">Musti</field>
</record>
</data>
</openerp>
+1 -4
View File
@@ -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
access_im_chat_shortcode,im_chat.shortcode,model_im_chat_shortcode,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_im_chat_session im_chat.session model_im_chat_session base.group_user 1 1 1 0
4 access_im_chat_conversation_state im_chat.conversation_state model_im_chat_conversation_state base.group_user 1 1 1 0
5 access_im_chat_presence im_chat.presence model_im_chat_presence base.group_user 1 1 1 1
6 access_im_chat_message_portal access_im_chat_shortcode im_chat.message im_chat.shortcode model_im_chat_message model_im_chat_shortcode base.group_portal base.group_user 1 0 1 1 0 1
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
@@ -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;
+63 -62
View File
@@ -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 = '<i class="'+icon_fa_class+'"></i> ' + label;
}
this.$('.oe_im_chatview_option_list').append('<li class="'+style_class+'">'+label+'</li>');
},
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 = {
":'(": "&#128546;",
":O" : "&#128561;",
"3:)": "&#128520;",
":)" : "&#128522;",
":D" : "&#128517;",
";)" : "&#128521;",
":p" : "&#128523;",
":(" : "&#9785;",
":|" : "&#128528;",
":/" : "&#128527;",
"8)" : "&#128563;",
":s" : "&#128534;",
":pinky" : "<img src='/im_chat/static/src/img/pinky.png'/>",
":musti" : "<img src='/im_chat/static/src/img/musti.png'/>",
};
if(kitten){
_.extend(smileys, {
":)" : "&#128570;",
":D" : "&#128569;",
";)" : "&#128572;",
":p" : "&#128573;",
":(" : "&#128576;",
":|" : "&#128575;",
});
}
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|$)"), ' <span class="smiley">'+smileys[key]+'</span> ');
});
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 += '<a href="' + url + '" target="_blank">' + url + '</a>';
}
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() {
@@ -7,6 +7,14 @@
<span class="oe_im_chatview_header_name"></span>
<span class="oe_im_chatview_nbr_messages"/>
<span class="oe_im_chatview_right">
<div class="btn-group oe_im_chatview_option_group">
<span class="oe_im_chatview_options dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-cogs"></i>
</span>
<ul class="dropdown-menu oe_im_chatview_option_list" role="menu">
<!-- options -->
</ul>
</div>
<div class="oe_im_chatview_close">×</div>
</span>
</div>
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="action_im_chat_shortcode" model="ir.actions.act_window">
<field name="name">Chat Shortcode</field>
<field name="res_model">im_chat.shortcode</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new chat shortcode.
</p><p>
A shortcode is a keyboard shortcut. For instance, you type #gm and it will be transformed into "Good Morning".
</p>
</field>
</record>
<record id="im_chat_shortcode_tree_view" model="ir.ui.view">
<field name="name">im_chat.shortcode.tree</field>
<field name="model">im_chat.shortcode</field>
<field name="arch" type="xml">
<tree string="Shortcodes">
<field name="source"/>
<field name="substitution"/>
<field name="description"/>
</tree>
</field>
</record>
<record id="im_chat_shortcode_form_view" model="ir.ui.view">
<field name="name">im_chat.shortcode.form</field>
<field name="model">im_chat.shortcode</field>
<field name="arch" type="xml">
<form string="Session Form" version="7.0">
<sheet>
<group colspan="2" col="2">
<field name="source"/>
<field name="substitution"/>
<field name="description"/>
</group>
</sheet>
</form>
</field>
</record>
</data>
</openerp>
+1
View File
@@ -1 +1,2 @@
import im_livechat
import report
+3 -1
View File
@@ -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",
+29
View File
@@ -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)
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<openerp>
<data noupdate="1">
<!-- After installation of the module, open the related menu -->
<record id="action_client_livechat_menu" model="ir.actions.client">
<field name="name">Open Livechat Channel Menu</field>
<field name="tag">reload</field>
<field name="params" eval="{'menu_id': ref('support_channels')}"/>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_client_livechat_menu"/>
<field name="state">open</field>
</record>
</data>
<data>
<!-- Custom Filters -->
<record id="filter_session_by_day" model="ir.filters">
<field name="name">Sessions by day</field>
<field name="model_id">im_livechat.report</field>
<field name="domain">[('nbr_speakers','&gt;', 1)]</field>
<field name="context">{'group_by': ['start_date:day', 'uuid']}</field>
<field name="is_default">v</field>
<field name="user_id" eval="False"/>
</record>
<record id="filter_session_last_24h" model="ir.filters">
<field name="name">Last 24h stats</field>
<field name="model_id">im_livechat.report</field>
<field name="domain">[('start_date','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') )]</field>
<field name="context">{'group_by': ['start_date:day', 'uuid']}</field>
<field name="user_id" eval="False"/>
</record>
<record id="filter_operator_stats" model="ir.filters">
<field name="name">Operators stats</field>
<field name="model_id">im_livechat.report</field>
<field name="domain">[('nbr_speakers','&gt;', 1), ('user_id', '!=', False)]</field>
<field name="context">{'group_by': ['start_date:month', 'user_id', 'uuid']}</field>
<field name="user_id" eval="False"/>
</record>
</data>
</openerp>
+1
View File
@@ -0,0 +1 @@
import im_livechat_report
@@ -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:
@@ -0,0 +1,56 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_livechat_support_session_graph">
<field name="name">im_livechat.report.graph</field>
<field name="model">im_livechat.report</field>
<field name="arch" type="xml">
<graph string="Livechat Support Statistics" type="pivot">
<field name="uuid" type="row"/>
<field name="duration" type="measure"/>
<field name="nbr_messages" type="measure"/>
<field name="time_to_answer" type="measure"/>
</graph>
</field>
</record>
<record id="im_livechat_session_search" model="ir.ui.view">
<field name="name">im_livechat.report.search</field>
<field name="model">im_livechat.report</field>
<field name="arch" type="xml">
<search string="Search report">
<filter name="missed_session" string="Missed sessions" domain="[('nbr_speakers','&lt;=', 1)]"/>
<filter name="treated_session" string="Treated sessions" domain="[('nbr_speakers','&gt;', 1)]"/>
<filter name="last_24h" string="Last 24h" domain="[('start_date','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%%Y-%%m-%%d') )]"/>
<group expand="0" string="Group By...">
<filter name="group_by_session" string="Session" domain="[]" context="{'group_by':'uuid'}"/>
<filter name="group_by_channel" string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter name="group_by_user" string="Operator" domain="[('user_id','!=', False)]" context="{'group_by':'user_id'}"/>
<separator orientation="vertical" />
<filter name="group_by_hour" string="Creation date (hour)" domain="[]" context="{'group_by':'start_date_hour'}"/>
<filter name="group_by_day" string="Creation date (day)" domain="[]" context="{'group_by':'start_date:day'}"/>
<filter name="group_by_week" string="Creation date (week)" domain="[]" context="{'group_by':'start_date:week'}"/>
<filter name="group_by_month" string="Creation date (month)" domain="[]" context="{'group_by':'start_date:month'}" />
<filter name="group_by_year" string="Creation date (year)" domain="[]" context="{'group_by':'start_date:year'}"/>
</group>
</search>
</field>
</record>
<record id="action_livechat_support_report" model="ir.actions.act_window">
<field name="name">Livechat Support Report</field>
<field name="res_model">im_livechat.report</field>
<field name="view_type">form</field>
<field name="view_mode">graph</field>
<field name="view_id" ref="view_livechat_support_session_graph"></field>
<field name="help">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.</field>
</record>
<menuitem id="menu_reporting_livechat" name="Livechat" parent="base.menu_reporting" sequence="50"/>
<menuitem name="Support Statistics" parent="menu_reporting_livechat" id="livechat_support_report_session" action="action_livechat_support_report" groups="group_im_livechat_manager" />
</data>
</openerp>
@@ -32,5 +32,16 @@
<field name="perm_create" eval="0"/>
</record>
<record id="session_rule_1" model="ir.rule">
<field name="name">Live Support Managers can see all session from live support</field>
<field name="model_id" ref="im_chat.model_im_chat_session"/>
<field name="groups" eval="[(6,0,[ref('im_livechat.group_im_livechat_manager')])]"/>
<field name="domain_force">[('to_id.channel_id', '!=', None)]</field>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
</data>
</openerp>
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ls_chann1 im_livechat.channel model_im_livechat_channel 1 0 0 0
3 access_ls_chann2 im_livechat.channel model_im_livechat_channel group_im_livechat 1 1 1 0
4 access_ls_chann3 im_livechat.channel model_im_livechat_channel group_im_livechat_manager 1 1 1 1
5 access_livechat_support_report im_livechat.report model_im_livechat_report group_im_livechat_manager 1 1 1 1
@@ -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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

@@ -20,6 +20,12 @@
this._super.apply(this, arguments);
this.shown = true;
this.loading_history = true; // since the session is kept after a refresh, anonymous can reload their history
this.feedback = false;
this.rating = false;
},
define_options: function(){
// no options for anonymous user
},
show: function(){
this._super.apply(this, arguments);
@@ -50,10 +56,52 @@
this.set('session', session);
openerp.set_cookie(im_livechat.COOKIE_NAME, JSON.stringify(session), 60*60);
},
click_close: function(e) {
this._super(e);
openerp.set_cookie(im_livechat.COOKIE_NAME, "", -1);
click_close: function(event) {
this.$('.oe_im_chatview_input').attr('disabled','disabled');
if(!this.feedback && (this.get('messages').length > 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'];
@@ -1,7 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="chatButton">
<t t-esc="widget.text"/>
</t>
<t t-name="chatButton">
<t t-esc="widget.text"/>
</t>
<t t-name="support_feedback">
<div class="oe_im_chatview_feedback">
<div class="oe_im_feedback_text">
Please take a second to rate our Livechat Support ...
</div>
<div class="oe_im_feedback_choices">
<a data-value="10" name="good" title="Good"><img src='/im_livechat/static/src/img/good.png'/></a>
<a data-value="5" name="ok" title="OK"><img src='/im_livechat/static/src/img/ok.png'/></a>
<a data-value="1" name="bad" title="Bad"><img src='/im_livechat/static/src/img/bad.png'/></a>
</div>
<div class="oe_im_feedback_small_text">
Click on the icon that represent the most your feeling.
</div>
<div class="oe_im_feedback_reason">
<textarea id="reason" placeholder="Explain your note"></textarea>
</div>
<div class="oe_im_feedback_btn">
<input type="button" id="submit" value="Send" />
</div>
</div>
</t>
</templates>
+2
View File
@@ -119,6 +119,8 @@
<script type="text/javascript" src="/im_livechat/static/src/js/im_livechat.js"></script>
<!-- CSS -->
<link rel="stylesheet" href="/im_chat/static/src/css/im_common.css"></link>
<!-- Style for livechat extern only -->
<link rel="stylesheet" href="/im_livechat/static/src/css/im_livechat.css"></link>
</template>
<!-- the js code to initialize the LiveSupport object -->
+66 -29
View File
@@ -3,7 +3,8 @@
<data>
<menuitem id="im_livechat" name="Live Chat" parent="mail.mail_feeds_main" groups="group_im_livechat"/>
<record model="ir.actions.act_window" id="action_support_channels">
<!-- Support Channel -->
<record id="action_support_channels" model="ir.actions.act_window">
<field name="name">Live Chat Channels</field>
<field name="res_model">im_livechat.channel</field>
<field name="view_mode">kanban,form</field>
@@ -23,7 +24,7 @@
<menuitem name="Channels" parent="im_livechat" id="support_channels" action="action_support_channels" groups="group_im_livechat"/>
<record model="ir.ui.view" id="support_channel_kanban">
<record id="support_channel_kanban" model="ir.ui.view">
<field name="name">support_channel.kanban</field>
<field name="model">im_livechat.channel</field>
<field name="arch" type="xml">
@@ -121,43 +122,79 @@
</field>
</record>
<record model="ir.actions.act_window" id="action_history">
<!-- Session History for Support -->
<record id="action_session_history" model="ir.actions.act_window">
<field name="name">History</field>
<field name="res_model">im_chat.message</field>
<field name="view_mode">list</field>
<field name="domain">[('to_id.channel_id', '!=', None)]</field>
<field name="context">{'search_default_group_by_to_id': 1}</field>
<field name="res_model">im_chat.session</field>
<field name="view_type">form</field>
<field name="view_mode">list,form</field>
<field name="domain">[('channel_id', '!=', None)]</field>
<field name="context">{'group_by': ['channel_id', 'create_date:month']}</field>
</record>
<menuitem name="History" parent="im_livechat" id="history" action="action_history" groups="group_im_livechat_manager"/>
<menuitem name="History" parent="im_livechat" id="session_history" action="action_session_history" groups="group_im_livechat"/>
<record id="im_message_form" model="ir.ui.view">
<field name="name">im.chat.message.tree</field>
<field name="model">im_chat.message</field>
<field name="arch" type="xml">
<tree string="History" create="false">
<field name="to_id"/>
<field name="create_date"/>
<field name="from_id"/>
<field name="message"/>
</tree>
</field>
</record>
<record id="im_message_search" model="ir.ui.view">
<field name="name">im.chat.message.search</field>
<field name="model">im_chat.message</field>
<record id="im_chat_session_search" model="ir.ui.view">
<field name="name">im_chat.session.search</field>
<field name="model">im_chat.session</field>
<field name="arch" type="xml">
<search string="Search history">
<filter name="to_id" string="My Sessions" domain="[('to_id.user_ids','in', uid)]"/>
<field name="from_id"/>
<field name="to_id"/>
<filter name="id" string="My sessions" domain="[('user_ids','in', uid)]"/>
<group expand="0" string="Group By...">
<filter name="group_by_to_id" string="Session" domain="[]" context="{'group_by':'to_id'}"/>
<filter name="group_by_date" string="Date" domain="[]" context="{'group_by':'create_date'}"/>
<filter name="group_by_channel" string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<separator orientation="vertical" />
<filter name="group_by_day" string="Creation date (day)" domain="[]" context="{'group_by':'create_date:day'}"/>
<filter name="group_by_week" string="Creation date (week)" domain="[]" context="{'group_by':'create_date:week'}"/>
<filter name="group_by_month" string="Creation date (month)" domain="[]" context="{'group_by':'create_date:month'}" />
<filter name="group_by_year" string="Creation date (year)" domain="[]" context="{'group_by':'create_date:year'}"/>
</group>
</search>
</field>
</record>
<record id="im_chat_session_list_view" model="ir.ui.view">
<field name="name">im_chat.session.tree</field>
<field name="model">im_chat.session</field>
<field name="arch" type="xml">
<tree string="History" create="false">
<field name="id"/>
<field name="fullname"/>
<field name="uuid"/>
<field name="create_date"/>
<field name="feedback_rating"/>
</tree>
</field>
</record>
<record id="im_chat_session_form_view" model="ir.ui.view">
<field name="name">im_chat.session.form</field>
<field name="model">im_chat.session</field>
<field name="arch" type="xml">
<form string="Session Form" version="7.0" create="false" edit="false">
<sheet>
<group>
<group string="General">
<field name="id"/>
<field name="uuid" readonly="1"/>
<field name="fullname" widget="html"/>
<field name="create_date" readonly="1"/>
</group>
<group string="Feedback">
<field name="feedback_rating"/>
<field name="feedback_reason"/>
</group>
</group>
<field name="message_ids" type="tree">
<tree string="History">
<field name="create_date" />
<field name="from_id" />
<field name="message" />
</tree>
</field>
</sheet>
</form>
</field>
</record>
</data>
</openerp>