diff --git a/addons/im_livechat/__init__.py b/addons/im_livechat/__init__.py index 781be8cfa8d..afed36aa655 100644 --- a/addons/im_livechat/__init__.py +++ b/addons/im_livechat/__init__.py @@ -1,2 +1,3 @@ +import controllers import im_livechat import report \ No newline at end of file diff --git a/addons/im_livechat/__openerp__.py b/addons/im_livechat/__openerp__.py index 0830333aa04..1783a8e52e4 100644 --- a/addons/im_livechat/__openerp__.py +++ b/addons/im_livechat/__openerp__.py @@ -14,6 +14,7 @@ Live Chat Support Allow to drop instant messaging widgets on any web page that will communicate with the current server and dispatch visitors request amongst several live chat operators. +Help your customers with this chat, and analyse their feedback. """, 'data': [ @@ -27,7 +28,7 @@ chat operators. 'demo': [ "im_livechat_demo.xml", ], - 'depends' : ["mail", "im_chat"], + 'depends' : ["mail", "rating", "im_chat"], 'installable': True, 'auto_install': False, 'application': True, diff --git a/addons/im_livechat/controllers/__init__.py b/addons/im_livechat/controllers/__init__.py new file mode 100644 index 00000000000..986caac7d6f --- /dev/null +++ b/addons/im_livechat/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +import livechat +import rating diff --git a/addons/im_livechat/controllers/livechat.py b/addons/im_livechat/controllers/livechat.py new file mode 100644 index 00000000000..c3015d33f87 --- /dev/null +++ b/addons/im_livechat/controllers/livechat.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +import json +import openerp +import openerp.addons.im_chat.im_chat + +from openerp import http +from openerp.http import request + + +class LivechatController(http.Controller): + + @http.route('/im_livechat/support//', type='http', auth='none') + def support_page(self, dbname, channel_id, **kwargs): + registry, cr, uid, context = openerp.modules.registry.RegistryManager.get(dbname), request.cr, openerp.SUPERUSER_ID, request.context + info = registry.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel_id) + info["dbname"] = dbname + info["channel"] = channel_id + info["channel_name"] = registry.get('im_livechat.channel').read(cr, uid, channel_id, ['name'], context=context)["name"] + return request.render('im_livechat.support_page', info) + + @http.route('/im_livechat/loader//', type='http', auth='none') + def loader(self, dbname, channel_id, **kwargs): + registry, cr, uid, context = openerp.modules.registry.RegistryManager.get(dbname), request.cr, openerp.SUPERUSER_ID, request.context + info = registry.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel_id) + info["dbname"] = dbname + info["channel"] = channel_id + info["username"] = kwargs.get("username", "Visitor") + # find the country from the request + country_id = False + country_code = request.session.geoip and request.session.geoip.get('country_name') or False + if country_code: + country_ids = registry.get('res.country').search(cr, uid, [('code', '=', country_code)], context=context) + if country_ids: + country_id = country_ids[0] + # extract url + url = request.httprequest.headers.get('Referer') or request.httprequest.base_url + # find the match rule for the given country and url + rule = registry.get('im_livechat.channel.rule').match_rule(cr, uid, channel_id, url, country_id, context=context) + if rule: + if rule.action == 'hide_button': + # don't return the initialization script, since its blocked (in the country) + return + rule_data = { + 'action' : rule.action, + 'auto_popup_timer' : rule.auto_popup_timer, + 'regex_url' : rule.regex_url, + } + info['rule'] = json.dumps(rule and rule_data or False) + return request.render('im_livechat.loader', info) + + @http.route('/im_livechat/get_session', type="json", auth="none") + def get_session(self, channel_id, anonymous_name, **kwargs): + cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db + reg = openerp.modules.registry.RegistryManager.get(db) + # if geoip, add the country name to the anonymous name + if request.session.geoip: + anonymous_name = anonymous_name + " ("+request.session.geoip.get('country_name', "")+")" + return reg.get("im_livechat.channel").get_channel_session(cr, uid, channel_id, anonymous_name, context=context) + + @http.route('/im_livechat/available', type='json', auth="none") + def available(self, db, channel): + cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db + reg = openerp.modules.registry.RegistryManager.get(db) + with reg.cursor() as cr: + return len(reg.get('im_livechat.channel').get_available_users(cr, uid, channel)) > 0 + diff --git a/addons/im_livechat/controllers/rating.py b/addons/im_livechat/controllers/rating.py new file mode 100644 index 00000000000..8623784b30b --- /dev/null +++ b/addons/im_livechat/controllers/rating.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +import openerp +from openerp.addons.rating.controllers.main import Rating +from openerp.addons.web import http +from openerp.http import request + +class LivechatRatingController(Rating): + + @http.route('/rating/livechat/feedback', type='json', auth='none') + def rating_livechat_feedback(self, uuid, rate, reason=None, **kwargs): + Session = request.env['im_chat.session'] + Rating = request.env['rating.rating'] + session_ids = Session.sudo().search([('uuid', '=', uuid)]) + if session_ids: + session = session_ids[0] + # limit the creation : only ONE rating per session + if not session.rating_ids: + values = { + 'res_id' : session.id, + 'res_model' : 'im_chat.session', + 'rating' : rate, + } + # find the partner related to the user of the conversation + rated_partner_id = False + if session.user_ids: + rated_partner_id = session.user_ids[0] and session.user_ids[0].partner_id.id or False + values['rated_partner_id'] = rated_partner_id + if reason: + values['feedback'] = reason + # create the rating + rating = Rating.sudo().create(values) + return rating.id + return False diff --git a/addons/im_livechat/im_livechat.py b/addons/im_livechat/im_livechat.py index 986021e72ad..43413e593b4 100644 --- a/addons/im_livechat/im_livechat.py +++ b/addons/im_livechat/im_livechat.py @@ -19,20 +19,13 @@ # ############################################################################## -import random import openerp -import json import openerp.addons.im_chat.im_chat -import datetime - +import random import re -import json 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): @@ -94,6 +87,24 @@ class im_livechat_channel(osv.Model): res[record.id] = len(record.session_ids) return res + # RATING METHOD + def _compute_percentage_satisfaction(self, cr, uid, ids, name, arg, context=None): + res = {} + for record in self.browse(cr, uid, ids, context=context): + repartition = record.session_ids.rating_get_grades() + total = sum(repartition.values()) + if total > 0: + happy = repartition['great'] + res[record.id] = ((happy*100) / total) if happy > 0 else 0 + else: + res[record.id] = -1 + return res + + def action_view_rating(self, cr, uid, ids, context=None): + channels = self.browse(cr, uid, ids, context=context) + action = self.pool['ir.actions.act_window'].for_xml_id(cr, uid, 'rating', 'action_view_rating', context=context) + action['domain'] = [('res_id', 'in', [s.id for s in channels.session_ids]), ('res_model', '=', 'im_chat.session')] + return action _columns = { 'name': fields.char(string="Channel Name", size=200, required=True), @@ -127,6 +138,8 @@ class im_livechat_channel(osv.Model): 'session_ids' : fields.one2many('im_chat.session', 'channel_id', 'Sessions'), 'nbr_session' : fields.function(_compute_nbr_session, type='integer', string='Number of session', store=False), 'rule_ids': fields.one2many('im_livechat.channel.rule','channel_id','Rules'), + # rating field + 'rating_percentage_satisfaction' : fields.function(_compute_percentage_satisfaction, type='integer', string='% Happy', store=False, default=-1), } def _default_user_ids(self, cr, uid, context=None): @@ -189,7 +202,6 @@ class im_livechat_channel(osv.Model): return True - class im_livechat_channel_rule(osv.Model): _name = 'im_livechat.channel.rule' @@ -227,7 +239,8 @@ class im_livechat_channel_rule(osv.Model): class im_chat_session(osv.Model): - _inherit = 'im_chat.session' + _name = 'im_chat.session' + _inherit = ['im_chat.session', 'rating.mixin'] def _get_fullname(self, cr, uid, ids, fields, arg, context=None): """ built the complete name of the session """ @@ -247,8 +260,6 @@ class im_chat_session(osv.Model): '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): @@ -282,68 +293,3 @@ class im_chat_session(osv.Model): else: return super(im_chat_session, self).quit_user(cr, uid, session.id, context=context) -class LiveChatController(http.Controller): - - @http.route('/im_livechat/support//', type='http', auth='none') - def support_page(self, dbname, channel_id, **kwargs): - registry, cr, uid, context = openerp.modules.registry.RegistryManager.get(dbname), request.cr, openerp.SUPERUSER_ID, request.context - info = registry.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel_id) - info["dbname"] = dbname - info["channel"] = channel_id - info["channel_name"] = registry.get('im_livechat.channel').read(cr, uid, channel_id, ['name'], context=context)["name"] - return request.render('im_livechat.support_page', info) - - @http.route('/im_livechat/loader//', type='http', auth='none') - def loader(self, dbname, channel_id, **kwargs): - registry, cr, uid, context = openerp.modules.registry.RegistryManager.get(dbname), request.cr, openerp.SUPERUSER_ID, request.context - info = registry.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel_id) - info["dbname"] = dbname - info["channel"] = channel_id - info["username"] = kwargs.get("username", "Visitor") - # find the country from the request - country_id = False - country_code = request.session.geoip and request.session.geoip.get('country_name', False) or False - if country_code: - country_ids = registry.get('res.country').search(cr, uid, [('code', '=', country_code)], context=context) - if country_ids: - country_id = country_ids[0] - # extract url - url = request.httprequest.headers.get('Referer', False) or request.httprequest.base_url - # find the match rule for the given country and url - rule = registry.get('im_livechat.channel.rule').match_rule(cr, uid, channel_id, url, country_id, context=context) - if rule: - if rule.action == 'hide_button': - # don't return the initialization script, since its blocked (in the country) - return - rule_data = { - 'action' : rule.action, - 'auto_popup_timer' : rule.auto_popup_timer, - 'regex_url' : rule.regex_url, - } - info['rule'] = json.dumps(rule and rule_data or False) - return request.render('im_livechat.loader', info) - - @http.route('/im_livechat/get_session', type="json", auth="none") - def get_session(self, channel_id, anonymous_name, **kwargs): - cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db - reg = openerp.modules.registry.RegistryManager.get(db) - # if geoip, add the country name to the anonymous name - if request.session.geoip: - anonymous_name = anonymous_name + " ("+request.session.geoip.get('country_name', "")+")" - return reg.get("im_livechat.channel").get_channel_session(cr, uid, channel_id, anonymous_name, context=context) - - @http.route('/im_livechat/available', type='json', auth="none") - def available(self, db, channel): - cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db - reg = openerp.modules.registry.RegistryManager.get(db) - 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/report/im_livechat_report.py b/addons/im_livechat/report/im_livechat_report.py index 44564f2d9d6..1237ee97333 100644 --- a/addons/im_livechat/report/im_livechat_report.py +++ b/addons/im_livechat/report/im_livechat_report.py @@ -18,7 +18,6 @@ class im_livechat_report(osv.Model): '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), @@ -42,7 +41,6 @@ class im_livechat_report(osv.Model): (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, S.id as session_id diff --git a/addons/im_livechat/static/src/css/im_livechat.css b/addons/im_livechat/static/src/css/im_livechat.css index 19a20483308..becceb34f7f 100644 --- a/addons/im_livechat/static/src/css/im_livechat.css +++ b/addons/im_livechat/static/src/css/im_livechat.css @@ -1,45 +1,42 @@ -/* feedback */ -.oe_im_chatview_feedback{ +/* Livechat Rating : feedback smiley */ +.oe_im_chatview .oe_livechat_rating{ padding: 15px; vertical-align: middle; } -.oe_im_chatview_feedback .oe_im_feedback_text { +.oe_im_chatview .oe_livechat_rating .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{ +.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices{ height:40px; text-align: center; - margin: 10px 0; + margin: 10px 0 30px 0; } -.oe_im_chatview_feedback .oe_im_feedback_choices a{ +.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img{ + width: 50px; + opacity: 0.60; cursor: pointer; margin: 5px; - opacity: 0.65; } -.oe_im_chatview_feedback .oe_im_feedback_choices a:hover{ +.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img:hover{ opacity:1; } -.oe_im_chatview_feedback .oe_im_feedback_choices a.selected{ +.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img.selected{ opacity: 1; } /* feedback reason */ -.oe_im_feedback_reason { +.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason { margin: 10px 0; + display: none; /* hidden by default */ } -.oe_im_feedback_reason textarea { +.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason textarea { width: 100%; height: 70px; resize: none; } -.oe_im_feedback_btn input{ +.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason_button 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 deleted file mode 100644 index 485a59bc3e5..00000000000 Binary files a/addons/im_livechat/static/src/img/bad.png and /dev/null differ diff --git a/addons/im_livechat/static/src/img/good.png b/addons/im_livechat/static/src/img/good.png deleted file mode 100644 index 9ae6cce415f..00000000000 Binary files a/addons/im_livechat/static/src/img/good.png and /dev/null differ diff --git a/addons/im_livechat/static/src/img/ok.png b/addons/im_livechat/static/src/img/ok.png deleted file mode 100644 index b79a83240d6..00000000000 Binary files a/addons/im_livechat/static/src/img/ok.png and /dev/null differ diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 188248b7d90..0ff35dde366 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -1,7 +1,3 @@ -/* - This file must compile in EcmaScript 3 and work in IE7. -*/ - (function() { "use strict"; @@ -20,9 +16,7 @@ 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; + this.feedback = false; }, define_options: function(){ // no options for anonymous user @@ -57,51 +51,17 @@ openerp.set_cookie(im_livechat.COOKIE_NAME, JSON.stringify(session), 60*60); }, 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)); + this.feedback = new im_livechat.Feedback(this); + this.$(".oe_im_chatview_content").empty(); + this.$(".oe_im_chatview_input").prop('disabled', true); + this.feedback.appendTo( this.$(".oe_im_chatview_content")); + // bind event to close conversation + this.feedback.on("feedback_sent", this, this.click_close); }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 @@ -138,11 +98,14 @@ openerp.session = new openerp.Session(null, server_url, { use_cors: false }); this.load_template(db, channel, options, rule); }, + _get_template_list: function(){ + return ['/im_chat/static/src/xml/im_chat.xml', '/im_livechat/static/src/xml/im_livechat.xml']; + }, load_template: function(db, channel, options, rule){ 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']; + var templates = this._get_template_list(); _.each(templates, function(tmpl){ defs.push(openerp.session.rpc('/web/proxy/load', {path: tmpl}).then(function(xml) { openerp.qweb.add_template(xml); @@ -191,7 +154,7 @@ this.no_session_message = _t("None of our collaborators seems to be available, please try again later."); }, start: function() { - this.$().append(openerp.qweb.render("chatButton", {widget: this})); + this.$().append(openerp.qweb.render("im_livechat.chatButton", {widget: this})); // set up the manager this.manager = new openerp.im_chat.ConversationManager(this, this.options); this.manager.set("bottom_offset", $('.oe_chat_button').outerHeight()); @@ -257,6 +220,48 @@ } }); + /* Rating livechat object */ + im_livechat.Feedback = openerp.Widget.extend({ + template : "im_livechat.feedback", + init: function(parent){ + this._super(parent); + this.conversation = parent; + this.reason = false; + this.rating = false; + }, + start: function(){ + this._super.apply(this.arguments); + // bind events + this.$('.oe_livechat_rating_choices img').on('click', _.bind(this.click_smiley, this)); + this.$('#rating_submit').on('click', _.bind(this.click_send, this)); + }, + click_smiley: function(ev){ + this.rating = parseInt($(ev.currentTarget).data('value')); + this.$('.oe_livechat_rating_choices img').removeClass('selected'); + this.$('.oe_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected'); + // only display textearea if bad smiley selected + if(this.rating == 0){ + this.$('.oe_livechat_rating_reason').show(); + }else{ + this.$('.oe_livechat_rating_reason').hide(); + } + this.$('textarea').val(''); // empty the reason each time a click on a smiley is done + }, + click_send: function(ev){ + this.reason = this.$('textarea').val(); + if(_.contains([0,5,10], this.rating)){ // need to use contains, since the rating can 0, evaluate to false + this._send_feedback(); + } + }, + _send_feedback: function(){ + var self = this; + var uuid = this.conversation.get('session').uuid; + openerp.session.rpc("/rating/livechat/feedback", {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) { + self.trigger("feedback_sent"); // will close the conversation + }); + } + }); + return im_livechat; })(); diff --git a/addons/im_livechat/static/src/xml/im_livechat.xml b/addons/im_livechat/static/src/xml/im_livechat.xml index 6e9d8f33520..1bbeefc0b0b 100644 --- a/addons/im_livechat/static/src/xml/im_livechat.xml +++ b/addons/im_livechat/static/src/xml/im_livechat.xml @@ -1,31 +1,29 @@ - + - + - -
+ +
- Please take a second to rate our Livechat Support ... + Did we correctly answer your question ?
-
- - - +
+ Good + OK + Bad
-
- Click on the icon that represent the most your feeling. -
-
+
-
- +
+
+ \ No newline at end of file diff --git a/addons/im_livechat/views/im_livechat.xml b/addons/im_livechat/views/im_livechat.xml index 2c8f299687d..76c97d57212 100644 --- a/addons/im_livechat/views/im_livechat.xml +++ b/addons/im_livechat/views/im_livechat.xml @@ -113,8 +113,7 @@ - - + diff --git a/addons/im_livechat/views/im_livechat_view.xml b/addons/im_livechat/views/im_livechat_view.xml index e5504636846..beb567e9e6f 100644 --- a/addons/im_livechat/views/im_livechat_view.xml +++ b/addons/im_livechat/views/im_livechat_view.xml @@ -40,7 +40,6 @@ - @@ -51,14 +50,32 @@
- - - - - - - - + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
+
+
@@ -176,6 +193,9 @@ +
@@ -266,10 +286,9 @@ - - - + +