[IMP] im_chat, im_livechat : add reporting view, quit conversation, option dropdown...
authorJérome Maes <jem@openerp.com>
Mon, 30 Jun 2014 14:50:24 +0000 (16:50 +0200)
committerJérome Maes <jem@openerp.com>
Fri, 3 Oct 2014 10:08:02 +0000 (12:08 +0200)
25 files changed:
addons/im_chat/__openerp__.py
addons/im_chat/im_chat.py
addons/im_chat/im_chat_data.xml [new file with mode: 0644]
addons/im_chat/security/ir.model.access.csv
addons/im_chat/static/src/css/im_common.css
addons/im_chat/static/src/js/im_chat.js
addons/im_chat/static/src/xml/im_chat.xml
addons/im_chat/views/im_chat_view.xml [new file with mode: 0644]
addons/im_livechat/__init__.py
addons/im_livechat/__openerp__.py
addons/im_livechat/im_livechat.py
addons/im_livechat/im_livechat_data.xml [new file with mode: 0644]
addons/im_livechat/report/__init__.py [new file with mode: 0644]
addons/im_livechat/report/im_livechat_report.py [new file with mode: 0644]
addons/im_livechat/report/im_livechat_report.xml [new file with mode: 0644]
addons/im_livechat/security/im_livechat_security.xml
addons/im_livechat/security/ir.model.access.csv
addons/im_livechat/static/src/css/im_livechat.css [new file with mode: 0644]
addons/im_livechat/static/src/img/bad.png [new file with mode: 0644]
addons/im_livechat/static/src/img/good.png [new file with mode: 0644]
addons/im_livechat/static/src/img/ok.png [new file with mode: 0644]
addons/im_livechat/static/src/js/im_livechat.js
addons/im_livechat/static/src/xml/im_livechat.xml
addons/im_livechat/views/im_livechat.xml
addons/im_livechat/views/im_livechat_view.xml

index 4b1ba54..b8f5151 100644 (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'],
index 4be9dc8..f865aed 100644 (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
diff --git a/addons/im_chat/im_chat_data.xml b/addons/im_chat/im_chat_data.xml
new file mode 100644 (file)
index 0000000..ecacc1f
--- /dev/null
@@ -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>
index c1caaf1..915c8a8 100644 (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
\ No newline at end of file
+access_im_chat_shortcode,im_chat.shortcode,model_im_chat_shortcode,base.group_user,1,1,1,1
index c89acb7..83dc9bb 100644 (file)
     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;
index 9962fdf..0d27e5c 100644 (file)
         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 = {
-                ":'(": "&#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);
         },
         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() {
index 0ca4fe3..5370635 100644 (file)
@@ -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>
diff --git a/addons/im_chat/views/im_chat_view.xml b/addons/im_chat/views/im_chat_view.xml
new file mode 100644 (file)
index 0000000..e43399c
--- /dev/null
@@ -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>
\ No newline at end of file
index e0f1b96..781be8c 100644 (file)
@@ -1 +1,2 @@
 import im_livechat
+import report
\ No newline at end of file
index 6f16c5b..0830333 100644 (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",
index 26f0876..dafc2ed 100644 (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)
+
+
diff --git a/addons/im_livechat/im_livechat_data.xml b/addons/im_livechat/im_livechat_data.xml
new file mode 100644 (file)
index 0000000..a489763
--- /dev/null
@@ -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>
\ 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 (file)
index 0000000..974388e
--- /dev/null
@@ -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 (file)
index 0000000..a0b2ce4
--- /dev/null
@@ -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 (file)
index 0000000..bc8d8df
--- /dev/null
@@ -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>
\ No newline at end of file
index d4cd790..8e000aa 100644 (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>
index 0b8be98..cb6097e 100644 (file)
@@ -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 (file)
index 0000000..19a2048
--- /dev/null
@@ -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 (file)
index 0000000..485a59b
Binary files /dev/null and b/addons/im_livechat/static/src/img/bad.png differ
diff --git a/addons/im_livechat/static/src/img/good.png b/addons/im_livechat/static/src/img/good.png
new file mode 100644 (file)
index 0000000..9ae6cce
Binary files /dev/null and b/addons/im_livechat/static/src/img/good.png differ
diff --git a/addons/im_livechat/static/src/img/ok.png b/addons/im_livechat/static/src/img/ok.png
new file mode 100644 (file)
index 0000000..b79a832
Binary files /dev/null and b/addons/im_livechat/static/src/img/ok.png differ
index 2d66509..802340d 100644 (file)
             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'];
index 1c7b004..6e9d8f3 100644 (file)
@@ -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>
\ No newline at end of file
index 6ff631a..dd9fdc9 100644 (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 -->
index fa18f4c..ee23c40 100644 (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">
             </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>