'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'],
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
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
'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
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,
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
--- /dev/null
+<?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">&#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">&#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">&#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">&#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">&#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">&#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">&#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">&#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">&#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">&#128569;</field>
+ <field name="description">Kitten</field>
+ </record>
+
+ <record id="im_chat_smiley_heart" model="im_chat.shortcode">
+ <field name="source">&lt;3</field>
+ <field name="substitution">&#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"><img src='/im_chat/static/src/img/pinky.png'/></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"><img src='/im_chat/static/src/img/musti.png'/></field>
+ <field name="description">Musti</field>
+ </record>
+
+
+ </data>
+</openerp>
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
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;
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"
},
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({
this.set("pending", this.get("pending") + 1);
}
this.insert_messages([message]);
+ this._go_bottom();
},
send_message: function(message, type) {
var self = this;
},
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;
});
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" : "<img src='/im_chat/static/src/img/pinky.png'/>",
- ":musti" : "<img src='/im_chat/static/src/img/musti.png'/>",
- };
- 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|$)"), ' <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);
},
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() {
<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>
--- /dev/null
+<?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>
\ No newline at end of file
import im_livechat
+import report
\ No newline at end of file
"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",
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'
return True
class im_chat_session(osv.Model):
+
_inherit = 'im_chat.session'
def _get_fullname(self, cr, uid, ids, fields, arg, context=None):
_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):
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):
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)
+
+
--- /dev/null
+<?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','>', 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','>', (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','>', 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>
\ No newline at end of file
--- /dev/null
+import im_livechat_report
\ No newline at end of file
--- /dev/null
+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:
--- /dev/null
+<?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','<=', 1)]"/>
+ <filter name="treated_session" string="Treated sessions" domain="[('nbr_speakers','>', 1)]"/>
+ <filter name="last_24h" string="Last 24h" domain="[('start_date','>', (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>
\ No newline at end of file
<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>
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
--- /dev/null
+/* 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;
+}
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);
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
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"),
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'];
<?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>
\ No newline at end of file
<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 -->
<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>
<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">
</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>
+ <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="id" string="My sessions" domain="[('user_ids','in', uid)]"/>
+ <group expand="0" string="Group By...">
+ <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="to_id"/>
+ <field name="id"/>
+ <field name="fullname"/>
+ <field name="uuid"/>
<field name="create_date"/>
- <field name="from_id"/>
- <field name="message"/>
+ <field name="feedback_rating"/>
</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_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">
- <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"/>
- <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'}"/>
- </group>
- </search>
+ <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>