[MIG] bus, im_chat, im_livechat, website_livechat : migration to new API.

Migrate the models and controller to new API. Renaming xml id according to convention, renaming openerp tag into odoo tag. Add comment strings and documentations.
This commit is contained in:
Jérome Maes
2015-04-28 17:04:16 +02:00
parent 7096fee53c
commit a80219e292
34 changed files with 911 additions and 849 deletions
+1 -2
View File
@@ -16,8 +16,7 @@ class BusController(openerp.http.Controller):
def send(self, channel, message):
if not isinstance(channel, basestring):
raise Exception("bus.Bus only string channels are allowed.")
registry, cr, uid = request.registry, request.cr, request.session.uid
return registry['bus.bus'].sendone(cr, uid, channel, message)
return request.env['bus.bus'].sendone(channel, message)
# override to add channels
def _poll(self, dbname, channels, last, options):
+51 -37
View File
@@ -2,19 +2,19 @@
import datetime
import json
import logging
import random
import select
import simplejson
import threading
import time
import random
import simplejson
import openerp
from openerp.osv import osv, fields
from openerp.http import request
from openerp import api, fields, models
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
_logger = logging.getLogger(__name__)
# longpolling timeout connection
TIMEOUT = 50
#----------------------------------------------------------
@@ -28,53 +28,67 @@ def hashable(key):
key = tuple(key)
return key
class ImBus(osv.Model):
_name = 'bus.bus'
_columns = {
'id' : fields.integer('Id'),
'create_date' : fields.datetime('Create date'),
'channel' : fields.char('Channel'),
'message' : fields.char('Message'),
}
def gc(self, cr, uid):
class ImBus(models.Model):
_name = 'bus.bus'
create_date = fields.Datetime('Create date')
channel = fields.Char('Channel')
message = fields.Char('Message')
@api.model
def gc(self):
timeout_ago = datetime.datetime.utcnow()-datetime.timedelta(seconds=TIMEOUT*2)
domain = [('create_date', '<', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
ids = self.search(cr, openerp.SUPERUSER_ID, domain)
self.unlink(cr, openerp.SUPERUSER_ID, ids)
return self.sudo().search(domain).unlink()
def sendmany(self, cr, uid, notifications):
@api.model
def sendmany(self, notifications):
channels = set()
for channel, message in notifications:
channels.add(channel)
values = {
"channel" : json_dump(channel),
"message" : json_dump(message)
"channel": json_dump(channel),
"message": json_dump(message)
}
self.pool['bus.bus'].create(cr, openerp.SUPERUSER_ID, values)
cr.commit()
self.sudo().create(values)
self.env.cr.commit()
if random.random() < 0.01:
self.gc(cr, uid)
self.gc()
if channels:
with openerp.sql_db.db_connect('postgres').cursor() as cr2:
cr2.execute("notify imbus, %s", (json_dump(list(channels)),))
def sendone(self, cr, uid, channel, message):
self.sendmany(cr, uid, [[channel, message]])
@api.model
def sendone(self, channel, message):
self.sendmany([[channel, message]])
def poll(self, cr, uid, channels, last=0):
@api.model
def poll(self, channels, last=0):
# first poll return the notification in the 'buffer'
if last == 0:
timeout_ago = datetime.datetime.utcnow()-datetime.timedelta(seconds=TIMEOUT)
domain = [('create_date', '>', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
else:
# else returns the unread notifications
domain = [('id','>',last)]
else: # else returns the unread notifications
domain = [('id', '>', last)]
channels = [json_dump(c) for c in channels]
domain.append(('channel','in',channels))
notifications = self.search_read(cr, openerp.SUPERUSER_ID, domain)
return [{"id":notif["id"], "channel": simplejson.loads(notif["channel"]), "message":simplejson.loads(notif["message"])} for notif in notifications]
domain.append(('channel', 'in', channels))
notifications = self.sudo().search_read(domain)
# list of notification to return
result = []
for notif in notifications:
result.append({
'id': notif['id'],
'channel': simplejson.loads(notif['channel']),
'message': simplejson.loads(notif['message']),
})
return result
#----------------------------------------------------------
# Dispatcher
#----------------------------------------------------------
class ImDispatch(object):
def __init__(self):
self.channels = {}
@@ -97,8 +111,8 @@ class ImDispatch(object):
# or wait for future ones
if not notifications:
event = self.Event()
for c in channels:
self.channels.setdefault(hashable(c), []).append(event)
for channel in channels:
self.channels.setdefault(hashable(channel), []).append(event)
try:
event.wait(timeout=timeout)
with registry.cursor() as cr:
@@ -116,7 +130,7 @@ class ImDispatch(object):
cr.execute("listen imbus")
cr.commit();
while True:
if select.select([conn], [], [], TIMEOUT) == ([],[],[]):
if select.select([conn], [], [], TIMEOUT) == ([], [], []):
pass
else:
conn.poll()
@@ -125,10 +139,10 @@ class ImDispatch(object):
channels.extend(json.loads(conn.notifies.pop().payload))
# dispatch to local threads/greenlets
events = set()
for c in channels:
events.update(self.channels.pop(hashable(c),[]))
for e in events:
e.set()
for channel in channels:
events.update(self.channels.pop(hashable(channel), []))
for event in events:
event.set()
def run(self):
while True:
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<template id="assets_backend" name="im assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
@@ -7,4 +7,4 @@
</xpath>
</template>
</data>
</openerp>
</odoo>
+39 -29
View File
@@ -1,47 +1,50 @@
# -*- coding: utf-8 -*
import base64
import openerp
import simplejson
from openerp import SUPERUSER_ID
from openerp.http import request
#----------------------------------------------------------
# Controllers
#----------------------------------------------------------
class Controller(openerp.addons.bus.controllers.main.BusController):
class ChatController(openerp.addons.bus.controllers.main.BusController):
def _default_request_uid(self):
""" For Anonymous people, they receive the access right of SUPERUSER_ID since they have NO access (auth=none)
!!! Each time a method from this controller is call, there is a check if the user (who can be anonymous and Sudo access)
can access to the ressource, thanks to 'is_in_session', (for instance).
"""
return request.session.uid and request.session.uid or SUPERUSER_ID
# --------------------------
# Extends BUS Controller Poll
# --------------------------
def _poll(self, dbname, channels, last, options):
if request.session.uid:
registry, cr, uid, context = request.registry, request.cr, request.session.uid, request.context
registry.get('im_chat.presence').update(cr, uid, options.get('im_presence', False), context=context)
request.env['im_chat.presence'].update(options.get('im_presence'))
# channel to receive message
channels.append((request.db, 'im_chat.session', request.session.uid))
## For performance issue, the real time status notification is disabled. This means a change of status are still braoadcasted
## but not received by anyone. Otherwise, all listening user restart their longpolling at the same time and cause a 'ConnectionPool Full Error'
## since there is not enought cursors for everyone. Now, when a user open his list of users, an RPC call is made to update his user status list.
##channels.append((request.db,'im_chat.presence'))
# channel to receive message
channels.append((request.db,'im_chat.session', request.uid))
return super(Controller, self)._poll(dbname, channels, last, options)
@openerp.http.route('/im_chat/init', type="json", auth="none")
def init(self):
registry, cr, uid, context = request.registry, request.cr, request.session.uid, request.context
notifications = registry['im_chat.message'].init_messages(cr, uid, context=context)
return notifications
return super(ChatController, self)._poll(dbname, channels, last, options)
# --------------------------
# Anonymous routes (Common Methods)
# --------------------------
@openerp.http.route('/im_chat/post', type="json", auth="none")
def post(self, uuid, message_type, message_content):
registry, cr, uid, context = request.registry, request.cr, request.session.uid, request.context
# execute the post method as SUPERUSER_ID
message_id = registry["im_chat.message"].post(cr, openerp.SUPERUSER_ID, uid, uuid, message_type, message_content, context=context)
return message_id
def post(self, uuid, message_content, message_type):
request_uid = self._default_request_uid()
return request.env["im_chat.message"].sudo(request_uid).send_message(request.session.uid, uuid, message_content, message_type)
@openerp.http.route(['/im_chat/image/<string:uuid>/<string:user_id>'], type='http', auth="none")
@openerp.http.route(['/im_chat/image/<string:uuid>/<int:user_id>', '/im_chat/image/<string:uuid>/<string:user_id>'], type='http', auth="none")
def image(self, uuid, user_id):
registry, cr, context, uid = request.registry, request.cr, request.context, request.session.uid
# get the image
Session = registry.get("im_chat.session")
image_b64 = Session.get_image(cr, openerp.SUPERUSER_ID, uuid, simplejson.loads(user_id), context)
# built the response
request_uid = self._default_request_uid()
# anonymous will have a user_id = False, interpreted by string
if isinstance(user_id, (basestring, unicode)):
user_id = False
# get image for the people in the channel
image_b64 = request.env['im_chat.session'].sudo(request_uid).session_user_image(uuid, user_id)
image_data = base64.b64decode(image_b64)
headers = [('Content-Type', 'image/png')]
headers.append(('Content-Length', len(image_data)))
@@ -49,5 +52,12 @@ class Controller(openerp.addons.bus.controllers.main.BusController):
@openerp.http.route(['/im_chat/history'], type="json", auth="none")
def history(self, uuid, last_id=False, limit=20):
registry, cr, uid, context = request.registry, request.cr, request.session.uid or openerp.SUPERUSER_ID, request.context
return registry["im_chat.message"].get_messages(cr, uid, uuid, last_id, limit, context=context)
request_uid = self._default_request_uid()
return request.env["im_chat.message"].sudo(request_uid).fetch_message(uuid, last_id, limit)
# --------------------------
# User routes (Only Logged User Methods)
# --------------------------
@openerp.http.route('/im_chat/init', type="json", auth="user")
def init(self):
return request.env['im_chat.message'].get_init_notifications()
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<openerp>
<data >
<odoo>
<data>
<!-- Smileys -->
<record id="im_chat_smiley_smile" model="im_chat.shortcode">
@@ -155,4 +155,4 @@
</record>
</data>
</openerp>
</odoo>
+350 -283
View File
@@ -1,202 +1,251 @@
# -*- coding: utf-8 -*-
import base64
import cgi
import datetime
import logging
import time
import uuid
import random
import re
import openerp
import cgi
import time
import uuid
from openerp import tools, _
from openerp.http import request
from openerp.osv import osv, fields
from openerp import api, fields, models
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.addons.bus.models.bus import TIMEOUT
from openerp.exceptions import UserError
_logger = logging.getLogger(__name__)
from openerp.addons.bus.models.bus import TIMEOUT
DISCONNECTION_TIMER = TIMEOUT + 5
AWAY_TIMER = 600 # 10 minutes
#----------------------------------------------------------
# Models
#----------------------------------------------------------
class im_chat_conversation_state(osv.Model):
""" Adds a state on the m2m between user and session. """
class ImChatConversationState(models.Model):
""" User Session State
Adds a state on the m2m between user and session.
"""
_name = 'im_chat.conversation_state'
_table = "im_chat_session_res_users_rel"
_description = 'Chat Conversation State'
_table = 'im_chat_session_res_users_rel'
_columns = {
"state" : fields.selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')]),
"session_id" : fields.many2one('im_chat.session', 'Session', required=True, ondelete="cascade"),
"user_id" : fields.many2one('res.users', 'Users', required=True, ondelete="cascade"),
}
_defaults = {
"state" : 'open'
}
state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Status', default='open')
session_id = fields.Many2one('im_chat.session', string='Session', required=True, ondelete='cascade')
user_id = fields.Many2one('res.users', string='Users', required=True, ondelete='cascade')
class ImChatSession(models.Model):
""" Session
Reprensenting a conversation between users. It manages anonymous user for method starting
with 'session_'.
"""
class im_chat_session(osv.Model):
""" Conversations."""
_order = 'id desc'
_name = 'im_chat.session'
_rec_name = 'uuid'
_description = 'Chat Conversation'
_order = 'id desc'
_columns = {
'uuid': fields.char('UUID', size=50, select=True),
'message_ids': fields.one2many('im_chat.message', 'to_id', 'Messages'),
'user_ids': fields.many2many('res.users', 'im_chat_session_res_users_rel', 'session_id', 'user_id', "Session Users"),
'session_res_users_rel': fields.one2many('im_chat.conversation_state', 'session_id', 'Relation Session Users'),
}
_defaults = {
'uuid': lambda *args: '%s' % uuid.uuid4(),
}
def _default_uuid(self):
return uuid.uuid4()
def is_in_session(self, cr, uid, uuid, user_id, context=None):
""" return if the given user_id is in the session """
sids = self.search(cr, uid, [('uuid', '=', uuid)], context=context, limit=1)
for session in self.browse(cr, uid, sids, context=context):
return user_id and user_id in [u.id for u in session.user_ids]
return False
uuid = fields.Char(string='UUID', index=True, default=_default_uuid)
message_ids = fields.One2many('im_chat.message', 'to_id', string='Messages')
session_res_users_rel = fields.One2many('im_chat.conversation_state', 'session_id', string='Relation Session Users')
user_ids = fields.Many2many('res.users', 'im_chat_session_res_users_rel', 'session_id', 'user_id', string='Session Users')
def users_infos(self, cr, uid, ids, context=None):
""" get the user infos for all the user in the session """
for session in self.pool["im_chat.session"].browse(cr, uid, ids, context=context):
users_infos = self.pool["res.users"].read(cr, uid, [u.id for u in session.user_ids], ['id','name', 'im_status'], context=context)
return users_infos
# --------------------------
# Session Methods
# --------------------------
@api.multi
def session_info(self):
""" Get the header of the current session
:returns the values of the session
:rtype : dict
"""
self.ensure_one()
info = {
'uuid': self.uuid,
'users': self.session_user_info(),
'state': 'open',
}
# add user session state, if available and if user is logged
if self._uid:
domain = [('user_id', '=', self._uid), ('session_id', 'in', self.ids)]
session_state = self.env['im_chat.conversation_state'].search(domain, limit=1)
if session_state:
info['state'] = session_state.state
return info
def is_private(self, cr, uid, ids, context=None):
for session_id in ids:
""" return true if the session is private between users no external messages """
mess_ids = self.pool["im_chat.message"].search(cr, uid, [('to_id','=',session_id),('from_id','=',None)], context=context)
return len(mess_ids) == 0
@api.multi
def session_user_info(self):
""" Get the user infos for all the identified user in the session
:returns a list of user infos
:rtype : list(dict)
"""
self.ensure_one()
return self.env['res.users'].browse(self.user_ids.ids).read(['id', 'name', 'im_status'])
def session_info(self, cr, uid, ids, context=None):
""" get the session info/header of a given session """
for session in self.browse(cr, uid, ids, context=context):
info = {
'uuid': session.uuid,
'users': session.users_infos(),
'state': 'open',
}
# add uid_state if available
if uid:
domain = [('user_id','=',uid), ('session_id','=',session.id)]
uid_state = self.pool['im_chat.conversation_state'].search_read(cr, uid, domain, ['state'], context=context)
if uid_state:
info['state'] = uid_state[0]['state']
return info
@api.model
def session_user_image(self, uuid, user_id):
""" Get the avatar of a user in the given session
:param uuid : the uuid of the session
:param user_id : the user identifier
"""
# get the session
image_b64 = False
if user_id:
session = self.env["im_chat.session"].search([('uuid', '=', uuid)], limit=1)
if session and session.sudo(user_id).is_in_session():
image_b64 = self.env["res.users"].sudo().browse(user_id).image_small
# set default image if not found
if not image_b64:
image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
return image_b64
def session_get(self, cr, uid, user_to, context=None):
""" returns the canonical session between 2 users, create it if needed """
session_id = False
@api.model
def session_get(self, user_to):
""" Get the canonical session between 2 users, create it if needed.
To reuse an old session, this one must be private, and contains only 2 users.
:param user_to : the identifier of the user
:returns a session header, or False if the user_to was False
:rtype : dict
"""
if user_to:
sids = self.search(cr, uid, [('user_ids','in', user_to),('user_ids', 'in', [uid])], context=context, limit=1)
for sess in self.browse(cr, uid, sids, context=context):
if len(sess.user_ids) == 2 and sess.is_private():
session_id = sess.id
break
else:
session_id = self.create(cr, uid, { 'user_ids': [(6,0, (user_to, uid))] }, context=context)
return self.session_info(cr, uid, [session_id], context=context)
session = self.search([('user_ids', 'in', user_to), ('user_ids', 'in', [self._uid])], limit=1)
if not (session and len(session.user_ids) == 2 and session.is_private()):
session = self.create({'user_ids': [(6, 0, (user_to, self._uid))]})
return session.session_info()
def update_state(self, cr, uid, uuid, state=None, context=None):
""" modify the fold_state of the given session, and broadcast to himself (e.i. : to sync multiple tabs) """
domain = [('user_id','=',uid), ('session_id.uuid','=',uuid)]
ids = self.pool['im_chat.conversation_state'].search(cr, uid, domain, context=context)
for sr in self.pool['im_chat.conversation_state'].browse(cr, uid, ids, context=context):
# --------------------------
# Utils Methods
# --------------------------
@api.multi
def is_private(self):
""" Return true if the session is private between identified users (no external messages).
The only way to do that is to check if there is a message without author (from_id is False).
"""
self.ensure_one()
message_ids = self.env["im_chat.message"].search([('to_id', 'in', self.ids), ('from_id', '=', None)])
return len(message_ids) == 0
@api.multi
def is_in_session(self):
""" Return True if the current user is in the user_ids of the session. False otherwise.
Note : the user need to be logged
"""
self.ensure_one()
user_id = self._uid
return user_id and user_id in [u.id for u in self.user_ids]
@api.model
def update_state(self, uuid, state=None):
""" Update the fold_state of the given session. In order to syncronize web browser
tabs, the change will be broadcast to himself (the current user channel).
Note : the user need to be logged
:param status : the new status of the session for the current user.
"""
domain = [('user_id', '=', self._uid), ('session_id.uuid', '=', uuid)]
for session_state in self.env['im_chat.conversation_state'].search(domain):
if not state:
state = sr.state
if sr.state == 'open':
state = session_state.state
if session_state.state == 'open':
state = 'folded'
else:
state = 'open'
self.pool['im_chat.conversation_state'].write(cr, uid, ids, {'state': state}, context=context)
self.pool['bus.bus'].sendone(cr, uid, (cr.dbname, 'im_chat.session', uid), sr.session_id.session_info())
session_state.write({'state': state})
self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.session', self._uid), session_state.session_id.session_info())
def add_user(self, cr, uid, uuid, user_id, context=None):
""" add the given user to the given session """
sids = self.search(cr, uid, [('uuid', '=', uuid)], context=context, limit=1)
for session in self.browse(cr, uid, sids, context=context):
if user_id not in [u.id for u in session.user_ids]:
self.write(cr, uid, [session.id], {'user_ids': [(4, user_id)]}, 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 is added : 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)
# send a message to the conversation
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)
# --------------------------
# User Actions
# --------------------------
@api.model
def add_user(self, uuid):
""" Add the current user to the session, and broadcast the meta message.
Note : the user need to be logged
:param uuid : the uuid of the session
"""
session = self.search([('uuid', '=', uuid)], limit=1)
if user_id not in [u.id for u in session.user_ids]:
session.write({'user_ids': [(4, self._uid)]})
# notify the all the channel users and anonymous channel
notifications = []
for user in session.user_ids:
info = session.sudo(user.id).session_info()
notifications.append([(self._cr.dbname, 'im_chat.session', user.id), info])
# anonymous are not notified when a new user is added : cannot execute session_info as uid = None
info = session.sudo().session_info()
notifications.append([session.uuid, info])
self.env['bus.bus'].sendmany(notifications)
# send a message to the conversation
user = self.env['res.users'].browse(self._uid)
message = _("%s joined the conversation.") % (user.name,)
self.env["im_chat.message"].send_message(session.uuid, "meta", message)
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)
@api.model
def quit_user(self, uuid):
""" Remove the current user from the given session.
Note : the user need to be logged
"""
session = self.search([('uuid', '=', uuid)], limit=1)
if self._uid in [u.id for u in session.user_ids] and len(session.user_ids) > 2:
return session._remove_user()
return False
@api.multi
def _remove_user(self):
""" Private implementation of removing the current user from the current session,
and broadcast the meta message.
Note : the user need to be logged
"""
self.ensure_one()
# 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)
user = self.env['res.users'].browse(self._uid)
message = _("%s left the conversation.") % (user.name,)
self.env["im_chat.message"].send_message(self.uuid, "meta", message)
# close his session state
self.update_state(self.uuid, 'closed')
# remove the user from session
self.write({"user_ids": [(3, self._uid)]})
# 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)
for user in self.user_ids:
info = self.sudo(user.id).session_info()
notifications.append([(self._cr.dbname, 'im_chat.session', user.id), info])
# anonymous are not notified when a new user left : cannot execute session_info as uid = None
info = self.sudo().session_info()
notifications.append([self.uuid, info])
self.env['bus.bus'].sendmany(notifications)
return True
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
image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
# get the session
if user_id:
session_id = self.pool["im_chat.session"].search(cr, uid, [('uuid','=',uuid), ('user_ids','in', user_id)])
if session_id:
# get the image of the user
res = self.pool["res.users"].read(cr, uid, [user_id], ["image_small"])[0]
if res["image_small"]:
image_b64 = res["image_small"]
return image_b64
class im_chat_message(osv.Model):
""" Sessions messsages type can be 'message' or 'meta'.
class ImChatMessage(models.Model):
""" Session Messsages
The chat provide 2 message type : 'message' (normal user message)
or 'meta' (message relative to changes about sessions)
For anonymous message, the from_id is False.
Messages are sent to a session not to users.
"""
_name = 'im_chat.message'
_order = "create_date asc"
_columns = {
'create_date': fields.datetime('Create Date', required=True, select=True),
'from_id': fields.many2one('res.users', 'Author'),
'to_id': fields.many2one('im_chat.session', 'Session To', required=True, select=True, ondelete='cascade'),
'type': fields.selection([('message','Message'), ('meta','Meta')], 'Type'),
'message': fields.char('Message'),
}
_defaults = {
'type' : 'message',
}
_name = 'im_chat.message'
_description = 'Chat Message'
_order = 'create_date asc'
@api.model
def _default_type(self):
return [('message', 'Message'), ('meta', 'Meta')]
create_date = fields.Datetime(string="Create date", required=True, index=True)
from_id = fields.Many2one('res.users', 'Author')
to_id = fields.Many2one('im_chat.session', 'Session', required=True, index=True)
type = fields.Selection('_default_type', 'Message type', default='message')
message = fields.Char('Message')
@api.model
def _escape_keep_url(self, message):
""" escape the message and transform the url into clickable link """
""" Escape the message and transform the url into clickable link
:param message : the message to escape url and transform them into clickable link
:returns the escaped message
"""
safe_message = ""
first = 0
last = 0
@@ -209,91 +258,116 @@ class im_chat_message(osv.Model):
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
@api.model
def get_init_notifications(self):
""" Get unread messages and old messages received less than AWAY_TIMER
ago and the session_info for open or folded window
Note : the user need to be logged
"""
# get the message since the AWAY_TIMER
threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
domain = [('to_id.user_ids', 'in', [uid]), ('create_date','>',threshold)]
domain = [('to_id.user_ids', 'in', [self._uid]), ('create_date', '>', threshold)]
# get the message since the last poll of the user
presence_ids = self.pool['im_chat.presence'].search(cr, uid, [('user_id', '=', uid)], context=context)
if presence_ids:
presence = self.pool['im_chat.presence'].browse(cr, uid, presence_ids, context=context)[0]
threshold = presence.last_poll
domain.append(('create_date','>',threshold))
messages = self.search_read(cr, uid, domain, ['from_id','to_id','create_date','type','message'], order='id asc', context=context)
presence = self.env['im_chat.presence'].search([('user_id', '=', self._uid)], limit=1)
if presence:
domain.append(('create_date', '>', presence.last_poll))
messages = self.search_read(domain, ['from_id', 'to_id', 'create_date', 'type', 'message'], order='id asc')
# get the session of the messages and the not-closed ones
session_ids = map(lambda m: m['to_id'][0], messages)
domain = [('user_id','=',uid), '|', ('state','!=','closed'), ('session_id', 'in', session_ids)]
session_rels_ids = self.pool['im_chat.conversation_state'].search(cr, uid, domain, context=context)
# re-open the session where a message have been recieve recently
session_rels = self.pool['im_chat.conversation_state'].browse(cr, uid, session_rels_ids, context=context)
session_ids = [m['to_id'][0] for m in messages]
domain = [('user_id', '=', self._uid), '|', ('state', '!=', 'closed'), ('session_id', 'in', session_ids)]
session_states = self.env['im_chat.conversation_state'].search(domain)
# re-open the session where a message have been recieved recently
session_states.filtered(lambda r: r.state == 'closed').write({'state': 'folded'})
reopening_session = []
# create the notifications (session infos first, then messages)
notifications = []
for sr in session_rels:
si = sr.session_id.session_info()
si['state'] = sr.state
if sr.state == 'closed':
si['state'] = 'folded'
reopening_session.append(sr.id)
notifications.append([(cr.dbname,'im_chat.session', uid), si])
for m in messages:
notifications.append([(cr.dbname,'im_chat.session', uid), m])
self.pool['im_chat.conversation_state'].write(cr, uid, reopening_session, {'state': 'folded'}, context=context)
for state in session_states:
session_infos = state.session_id.session_info()
notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), session_infos])
for message in messages:
notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), message])
return notifications
def post(self, cr, uid, from_uid, uuid, message_type, message_content, context=None):
""" post and broadcast a message, return the message id """
message_id = False
Session = self.pool['im_chat.session']
session_ids = Session.search(cr, uid, [('uuid','=',uuid)], context=context)
@api.model
def send_message(self, from_uid, uuid, message_content, message_type='message'):
""" Post and broadcast a message for the given session, return the message id
:param from_uid : user id of the author of the message
:param uuid : the uuid of the session
:param message_content : the content of the message
:param message_type : the type of the message
:returns the identifier of the new created message
:rtype : integer
"""
session = self.env['im_chat.session'].search([('uuid', '=', uuid)], limit=1)
# escape the new message
message_content = self._escape_keep_url(message_content)
message_content = self.env['im_chat.shortcode'].replace_shortcode(message_content)
# create the chat message
values = {
"from_id": from_uid,
"to_id": session.id,
"type": message_type,
"message": message_content,
}
message = self.create(values)
# broadcast the message
data = message.read(['from_id', 'to_id', 'create_date', 'type', 'message'])[0]
notifications = []
for session in Session.browse(cr, uid, session_ids, context=context):
# 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)
notifications.append([uuid, data])
for user in session.sudo().user_ids: # sudo required for portal user
notifications.append([(self._cr.dbname, 'im_chat.session', user.id), data])
self.env['bus.bus'].sendmany(notifications)
return message.id
vals = {
"from_id": from_uid,
"to_id": session.id,
"type": message_type,
"message": message_content,
}
# save it
message_id = self.create(cr, uid, vals, context=context)
# broadcast it to channel (anonymous users) and users_ids
data = self.read(cr, uid, [message_id], ['from_id','to_id','create_date','type','message'], context=context)[0]
notifications.append([uuid, data])
for user in session.user_ids:
notifications.append([(cr.dbname, 'im_chat.session', user.id), data])
self.pool['bus.bus'].sendmany(cr, uid, notifications)
return message_id
def get_messages(self, cr, uid, uuid, last_id=False, limit=20, context=None):
""" get messages (id desc) from given last_id in the given session """
Session = self.pool['im_chat.session']
if Session.is_in_session(cr, uid, uuid, uid, context=context):
@api.model
def fetch_message(self, uuid, last_id=False, limit=20):
""" Return messages for the given session uuid. If the session is public, fetch_message
as 'sudo', else (private session) fetch them as the current user. The security rule
'message_rule_1' will check the access right.
:param uuid : uuid of the session to fetch messages
:param last_id : last message id to start the research
:param limit : maximum number of messages to fetch
:returns list of messages values
:rtype : list(dict)
"""
session = self.env['im_chat.session'].search([('uuid', '=', uuid)], limit=1)
if session and session.is_in_session():
domain = [("to_id.uuid", "=", uuid)]
if last_id:
domain.append(("id", "<", last_id));
return self.search_read(cr, uid, domain, ['id', 'create_date','to_id','from_id', 'type', 'message'], limit=limit, order="id desc", context=context)
domain.append(("id", "<", last_id))
return self.search_read(domain, ['id', 'create_date', 'to_id', 'from_id', 'type', 'message'], limit=limit, order="id desc")
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'),
}
class ImChatShortcode(models.Model):
""" Shortcode
Canned Responses, allowing the user to defined shortcuts in its chat message.
These shortcode are globals and are available for every user. Smiley use this mecanism.
"""
_name = 'im_chat.shortcode'
_description = 'Canned Response / Shortcode'
source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages")
substitution = fields.Char('Substitution', required=True, index=True, help="The excaped html code replacing the shortcut")
description = fields.Char('Description')
@api.model
def create(self, values):
if values.get('substitution'):
values['substitution'] = self._sanitize_shorcode(values['substitution'])
return super(ImChatShortcode, self).create(values)
@api.multi
def write(self, values):
if values.get('substitution'):
values['substitution'] = self._sanitize_shorcode(values['substitution'])
return super(ImChatShortcode, self).write(values)
def _sanitize_shorcode(self, substitution):
""" Sanitize the shortcode substitution :
@@ -305,91 +379,84 @@ class im_chat_shortcode(osv.Model):
return substitution
return cgi.escape(substitution)
def create(self, cr, uid, values, context=None):
if values.get('substitution'):
values['substitution'] = self._sanitize_shorcode(values['substitution'])
return super(im_chat_shortcode, self).create(cr, uid, values, context=context)
def write(self, cr, uid, ids, values, context=None):
if values.get('substitution'):
values['substitution'] = self._sanitize_shorcode(values['substitution'])
return super(im_chat_shortcode, self).write(cr, uid, ids, values, context=context)
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)
@api.model
def replace_shortcode(self, message):
for shortcode in self.search([]):
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
class ImChatPresence(models.Model):
""" User Chat Presence
Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not
attached to res_users to avoid database concurrence errors. Since the 'update' method is executed
at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'.
"""
_name = 'im_chat.presence'
_description = 'User Chat Presence'
_columns = {
'user_id' : fields.many2one('res.users', 'Users', required=True, select=True, ondelete="cascade"),
'last_poll': fields.datetime('Last Poll'),
'last_presence': fields.datetime('Last Presence'),
'status' : fields.selection([('online','Online'), ('away','Away'), ('offline','Offline')], 'IM Status'),
}
_defaults = {
'last_poll' : fields.datetime.now,
'last_presence' : fields.datetime.now,
'status' : 'offline'
}
_sql_constraints = [('im_chat_user_status_unique','unique(user_id)', 'A user can only have one IM status.')]
_sql_constraints = [('im_chat_user_status_unique', 'unique(user_id)', 'A user can only have one IM status.')]
def update(self, cr, uid, presence=True, context=None):
""" register the poll, and change its im status if necessary. It also notify the Bus if the status has changed. """
presence_ids = self.search(cr, uid, [('user_id', '=', uid)], context=context)
presences = self.browse(cr, uid, presence_ids, context=context)
user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade')
last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now())
last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now())
status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline')
@api.model
def update(self, user_presence=True):
""" Register the given presence of the current user, and trigger a im_status change if necessary.
The status will not be written or sent if not necessary.
:param user_presence : True, if the user (self._uid) is still detected using its browser.
:type user_presence : boolean
"""
presence = self.search([('user_id', '=', self._uid)], limit=1)
# set the default values
send_notification = True
vals = {
values = {
'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'status' : presences and presences[0].status or 'offline'
'status' : presence and presence.status or 'offline'
}
# update the user or a create a new one
if not presences:
vals['status'] = 'online'
vals['user_id'] = uid
self.create(cr, uid, vals, context=context)
else:
if presence:
vals['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
vals['status'] = 'online'
if not presence: # create a new presence for the user
values['status'] = 'online'
values['user_id'] = self._uid
self.create(values)
else: # write the user presence if necessary
if user_presence:
values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
values['status'] = 'online'
else:
threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
if datetime.datetime.strptime(presences[0].last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold:
vals['status'] = 'away'
send_notification = presences[0].status != vals['status']
if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold:
values['status'] = 'away'
send_notification = presence.status != values['status']
# write only if the last_poll is passed TIMEOUT, or if the status has changed
delta = datetime.datetime.now() - datetime.datetime.strptime(presences[0].last_poll, DEFAULT_SERVER_DATETIME_FORMAT)
if (delta > datetime.timedelta(seconds=TIMEOUT) or send_notification):
delta = datetime.datetime.utcnow() - datetime.datetime.strptime(presence.last_poll, DEFAULT_SERVER_DATETIME_FORMAT)
if delta > datetime.timedelta(seconds=TIMEOUT) or send_notification:
# Hide transaction serialization errors, which can be ignored, the presence update is not essential
with tools.mute_logger('openerp.sql_db'):
self.write(cr, uid, presence_ids, vals, context=context)
presence.write(values)
# avoid TransactionRollbackError
cr.commit()
self.env.cr.commit() # TODO : check if still necessary
# notify if the status has changed
if send_notification:
self.pool['bus.bus'].sendone(cr, uid, (cr.dbname,'im_chat.presence'), {'id': uid, 'im_status': vals['status']})
if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence
self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.presence'), {'id': self._uid, 'im_status': values['status']})
# gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it.
if random.random() < 0.01:
self.check_users_disconnection(cr, uid, context=context)
self.check_users_disconnection()
return True
def check_users_disconnection(self, cr, uid, context=None):
""" disconnect the users having a too old last_poll """
dt = (datetime.datetime.now() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
presence_ids = self.search(cr, uid, [('last_poll', '<', dt), ('status' , '!=', 'offline')], context=context)
self.write(cr, uid, presence_ids, {'status': 'offline'}, context=context)
presences = self.browse(cr, uid, presence_ids, context=context)
@api.model
def check_users_disconnection(self):
""" Disconnect the users having a too old last_poll """
limit_date = (datetime.datetime.utcnow() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')])
presences.write({'status': 'offline'})
notifications = []
for presence in presences:
notifications.append([(cr.dbname,'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}])
self.pool['bus.bus'].sendmany(cr, uid, notifications)
return True
notifications.append([(self._cr.dbname, 'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}])
self.env['bus.bus'].sendmany(notifications)
+33 -30
View File
@@ -1,30 +1,33 @@
# -*- coding: utf-8 -*-
from openerp.osv import osv, fields
from openerp import api, fields, models
class ResUsers(models.Model):
class res_users(osv.osv):
_inherit = "res.users"
def _get_im_status(self, cr, uid, ids, fields, arg, context=None):
""" function computing the im_status field of the users """
r = dict((i, 'offline') for i in ids)
status_ids = self.pool['im_chat.presence'].search(cr, uid, [('user_id', 'in', ids)], context=context)
status = self.pool['im_chat.presence'].browse(cr, uid, status_ids, context=context)
for s in status:
r[s.user_id.id] = s.status
return r
im_status = fields.Char('IM Status', compute='_compute_im_status')
_columns = {
'im_status' : fields.function(_get_im_status, type="char", string="IM Status"),
}
@api.multi
def _compute_im_status(self):
""" Compute the im_status of the users """
res = dict((uid, 'offline') for uid in self.ids)
presences = self.env['im_chat.presence'].search([('user_id', 'in', self.ids)])
for presence in presences:
res[presence.user_id.id] = presence.status
for user in self:
user.im_status = res.get(user.id)
def im_search(self, cr, uid, name, limit=20, context=None):
""" search users with a name and return its id, name and im_status """
result = [];
@api.model
def im_search(self, name, limit=20):
""" Search users with a name and return its id, name and im_status.
Note : the user must be logged
:param name : the user name to search
:param limit : the limit of result to return
"""
result = []
# find the employee group
group_employee = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base', 'group_user')[1]
group_employee = self.env['ir.model.data'].get_object_reference('base', 'group_user')[1]
where_clause_base = " U.active = 't' "
query_params = ()
@@ -33,7 +36,7 @@ class res_users(osv.osv):
query_params = query_params + ('%'+name+'%',)
# first query to find online employee
cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
FROM im_chat_presence S
JOIN res_users U ON S.user_id = U.id
JOIN res_partner P ON P.id = U.partner_id
@@ -43,12 +46,12 @@ class res_users(osv.osv):
AND S.status = 'online'
ORDER BY P.name
LIMIT %s
''', query_params + (uid, group_employee, limit))
result = result + cr.dictfetchall()
''', query_params + (self._uid, group_employee, limit))
result = result + self._cr.dictfetchall()
# second query to find other online people
if(len(result) < limit):
cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
if len(result) < limit:
self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
FROM im_chat_presence S
JOIN res_users U ON S.user_id = U.id
JOIN res_partner P ON P.id = U.partner_id
@@ -57,12 +60,12 @@ class res_users(osv.osv):
AND S.status = 'online'
ORDER BY P.name
LIMIT %s
''', query_params + (tuple([u["id"] for u in result]) + (uid,), limit-len(result)))
result = result + cr.dictfetchall()
''', query_params + (tuple([u["id"] for u in result]) + (self._uid,), limit-len(result)))
result = result + self._cr.dictfetchall()
# third query to find all other people
if(len(result) < limit):
cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
if len(result) < limit:
self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
FROM res_users U
LEFT JOIN im_chat_presence S ON S.user_id = U.id
LEFT JOIN res_partner P ON P.id = U.partner_id
@@ -70,6 +73,6 @@ class res_users(osv.osv):
AND U.id NOT IN %s
ORDER BY P.name
LIMIT %s
''', query_params + (tuple([u["id"] for u in result]) + (uid,), limit-len(result)))
result = result + cr.dictfetchall()
return result
''', query_params + (tuple([u["id"] for u in result]) + (self._uid,), limit-len(result)))
result = result + self._cr.dictfetchall()
return result
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<openerp>
<odoo>
<data>
<record id="message_rule_1" model="ir.rule">
<record id="im_chat_message_rule_user" model="ir.rule">
<field name="name">Can only read messages that you sent or messages sent to you</field>
<field name="model_id" ref="model_im_chat_message"/>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
@@ -12,7 +12,7 @@
<field name="perm_create" eval="1"/>
</record>
<record id="users_rule_1" model="ir.rule">
<record id="im_chat_session_rule_user" model="ir.rule">
<field name="name">Can only modify your session</field>
<field name="model_id" ref="model_im_chat_session"/>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
@@ -23,7 +23,7 @@
<field name="perm_create" eval="1"/>
</record>
<record id="session_relation_rule_1" model="ir.rule">
<record id="im_chat_session_relation_rule_user" model="ir.rule">
<field name="name">Can only modify your own session relations</field>
<field name="model_id" ref="model_im_chat_conversation_state"/>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
@@ -34,4 +34,4 @@
<field name="perm_create" eval="1"/>
</record>
</data>
</openerp>
</odoo>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<template id="assets_backend" name="im_chat assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
@@ -11,4 +10,4 @@
</xpath>
</template>
</data>
</openerp>
</odoo>
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="action_im_chat_shortcode" model="ir.actions.act_window">
<!-- im_chat.shortcode -->
<record id="im_chat_shortcode_action" 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>
@@ -15,7 +16,7 @@
</field>
</record>
<record id="im_chat_shortcode_tree_view" model="ir.ui.view">
<record id="im_chat_shortcode_view_tree" model="ir.ui.view">
<field name="name">im_chat.shortcode.tree</field>
<field name="model">im_chat.shortcode</field>
<field name="arch" type="xml">
@@ -27,7 +28,7 @@
</field>
</record>
<record id="im_chat_shortcode_form_view" model="ir.ui.view">
<record id="im_chat_shortcode_view_form" model="ir.ui.view">
<field name="name">im_chat.shortcode.form</field>
<field name="model">im_chat.shortcode</field>
<field name="arch" type="xml">
@@ -44,4 +45,4 @@
</record>
</data>
</openerp>
</odoo>
+20 -25
View File
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
import json
import openerp
import openerp.addons.im_chat.im_chat
from openerp import http
from openerp import http, _
from openerp import SUPERUSER_ID
from openerp.http import request
@@ -11,31 +10,30 @@ class LivechatController(http.Controller):
@http.route('/im_livechat/support/<string:dbname>/<int:channel_id>', 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)
Channel = request.env['im_livechat.channel']
info = Channel.get_channel_infos(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"]
info["channel_name"] = Channel.browse(channel_id).name
return request.render('im_livechat.support_page', info)
@http.route('/im_livechat/loader/<string:dbname>/<int:channel_id>', 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 = request.env['im_livechat.channel'].sudo().get_channel_infos(channel_id)
info["dbname"] = dbname
info["channel"] = channel_id
info["username"] = kwargs.get("username", "Visitor")
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_code') 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]
countries = request.env['res.country'].search([('code', '=', country_code)])
if countries:
country_id = countries[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)
rule = request.env['im_livechat.channel.rule'].match_rule(channel_id, url, country_id)
if rule:
if rule.action == 'hide_button':
# don't return the initialization script, since its blocked (in the country)
@@ -50,22 +48,19 @@ class LivechatController(http.Controller):
@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.session.uid, 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', "")+")"
# if the user is identifiy (eg: portal user on the frontend), don't use the anonymous name. The user will be added to session.
if request.session.uid:
anonymous_name = False
return reg.get("im_livechat.channel").get_channel_session(cr, uid, channel_id, anonymous_name, context=context)
return request.env['im_livechat.channel'].sudo(request.session.uid).get_channel_session(channel_id, anonymous_name)
@http.route('/im_livechat/available', type='json', auth="none")
def available(self, db, channel):
cr, uid, context, db = request.cr, request.session.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
user_id = request.session.uid or SUPERUSER_ID
channel = request.env['im_livechat.channel'].sudo(user_id).browse(channel)
return len(channel.get_available_users()) > 0
@http.route('/rating/livechat/feedback', type='json', auth='none')
def rating_livechat_feedback(self, uuid, rate, reason=None, **kwargs):
@@ -76,14 +71,14 @@ class LivechatController(http.Controller):
session = session_ids[0]
# limit the creation : only ONE rating per session
values = {
'rating' : rate,
'rating': rate,
}
if not session.rating_ids:
values.update({
'res_id' : session.id,
'res_model' : 'im_chat.session',
'rating' : rate,
'feedback' : reason,
'res_id': session.id,
'res_model': 'im_chat.session',
'rating': rate,
'feedback': reason,
})
# find the partner related to the user of the conversation
rated_partner_id = False
@@ -1,16 +1,15 @@
<?xml version="1.0"?>
<openerp>
<odoo>
<data noupdate="1">
<!-- After installation of the module, open the related menu -->
<record id="action_client_livechat_menu" model="ir.actions.client">
<record id="ir_action_client_open_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="action_id" ref="ir_action_client_open_livechat_menu"/>
<field name="state">open</field>
</record>
@@ -30,7 +29,7 @@
<data>
<!-- Custom Filters -->
<record id="filter_session_by_day" model="ir.filters">
<record id="im_livechat_report_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>
@@ -39,7 +38,7 @@
<field name="user_id" eval="False"/>
</record>
<record id="filter_session_last_24h" model="ir.filters">
<record id="im_livechat_report_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>
@@ -47,7 +46,7 @@
<field name="user_id" eval="False"/>
</record>
<record id="filter_operator_stats" model="ir.filters">
<record id="im_livechat_report_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>
@@ -57,19 +56,19 @@
<!-- Smiley for rating to be displayed in the conversation -->
<record id="im_livechat_rating_0" model="im_chat.shortcode">
<record id="im_chat_shortcode_rating_0" model="im_chat.shortcode">
<field name="source">:rating_0</field>
<field name="substitution">&lt;img src='/rating/static/src/img/rating_0.png'/&gt;</field>
<field name="description">Rating Smiley Bad</field>
</record>
<record id="im_livechat_rating_5" model="im_chat.shortcode">
<record id="im_chat_shortcode_rating_5" model="im_chat.shortcode">
<field name="source">:rating_5</field>
<field name="substitution">&lt;img src='/rating/static/src/img/rating_5.png'/&gt;</field>
<field name="description">Rating Smiley Okay</field>
</record>
<record id="im_livechat_rating_10" model="im_chat.shortcode">
<record id="im_chat_shorcode_rating_10" model="im_chat.shortcode">
<field name="source">:rating_10</field>
<field name="substitution">&lt;img src='/rating/static/src/img/rating_10.png'/&gt;</field>
<field name="description">Rating Smiley Good</field>
@@ -77,4 +76,4 @@
</data>
</openerp>
</odoo>
@@ -1,22 +1,20 @@
<?xml version="1.0"?>
<openerp>
<odoo>
<data>
<record id="channel_website" model="im_livechat.channel">
<record id="im_livechat_channel_demo" model="im_livechat.channel">
<field name="name">YourWebsite.com</field>
<field name="default_message">Hello, how may I help you?</field>
</record>
<record id="group_im_livechat" model="res.groups">
<field name="users" eval="[(4, ref('base.user_demo'))]"/>
</record>
<record id="channel_rule" model="im_livechat.channel.rule">
<record id="im_livechat_channel_rule_demo" model="im_livechat.channel.rule">
<field name="regex_url">/im_livechat/</field>
<field name="action">auto_popup</field>
<field name="auto_popup_timer">3</field>
<field name="channel_id" ref="channel_website" />
<field name="channel_id" ref="im_livechat_channel_demo" />
</record>
<record id="im_livechat.im_livechat_group_user" model="res.groups">
<field name="users" eval="[(4, ref('base.user_demo'))]"/>
</record>
</data>
</openerp>
</odoo>
+60 -49
View File
@@ -1,67 +1,78 @@
# -*- coding: utf-8 -*-
import openerp
from openerp import api, fields, models
from openerp import SUPERUSER_ID
from openerp.osv import osv, fields
class im_chat_session(osv.Model):
class ImChatSession(models.Model):
""" Chat Session
Reprensenting a conversation between users.
It extends the base method for anonymous usage.
"""
_name = 'im_chat.session'
_inherit = ['im_chat.session', 'rating.mixin']
def _get_fullname(self, cr, uid, ids, fields, arg, context=None):
anonymous_name = fields.Char('Anonymous Name')
create_date = fields.Datetime('Create Date', required=True)
channel_id = fields.Many2one('im_livechat.channel', 'Channel')
fullname = fields.Char('Complete Name', compute='_compute_fullname')
@api.multi
@api.depends('anonymous_name', 'user_ids')
def _compute_fullname(self):
""" built the complete name of the session """
result = {}
sessions = self.browse(cr, uid, ids, context=context)
for session in sessions:
for session in self:
names = []
for user in session.user_ids:
names.append(user.name)
if session.anonymous_name:
names.append(session.anonymous_name)
result[session.id] = ', '.join(names)
return result
session.fullname = ', '.join(names)
_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"),
}
@api.multi
def is_in_session(self):
""" Return True if the current user is in the user_ids of the session. False otherwise.
If this is executed as sudo, this might be the anonymous user.
"""
self.ensure_one()
if self.anonymous_name and self._uid == SUPERUSER_ID:
return True
else:
return super(ImChatSession, self).is_in_session()
def is_in_session(self, cr, uid, uuid, user_id, context=None):
""" return if the given user_id is in the session """
sids = self.search(cr, uid, [('uuid', '=', uuid)], context=context, limit=1)
for session in self.browse(cr, uid, sids, context=context):
if session.anonymous_name and user_id == openerp.SUPERUSER_ID:
@api.multi
def session_user_info(self):
""" Get the user infos for all the identified user in the session + the anonymous if anonymous session
:returns a list of user infos
:rtype : list(dict)
"""
self.ensure_one()
users_infos = super(ImChatSession, self).session_user_info()
if self.anonymous_name:
users_infos.append({'id': False, 'name': self.anonymous_name, 'im_status': 'online'})
return users_infos
@api.model
def quit_user(self, uuid):
""" Remove the current user from the given session.
Note : an anonymous user cannot leave the session, since he is not registered.
Required to modify the base comportement, since a session can contain only 1 identified user.
:param uuid : the uuid of the session to quit
"""
session = self.search([('uuid', '=', uuid)], limit=1)
if session.anonymous_name:
# an identified user can leave an anonymous session if there is still another idenfied user in it
if self._uid and self._uid in [u.id for u in session.user_ids] and len(session.user_ids) > 1:
self._remove_user()
return True
else:
return super(im_chat_session, self).is_in_session(cr, uid, uuid, user_id, context=context)
return False
return False
else:
return super(ImChatSession, self).quit_user(uuid)
def users_infos(self, cr, uid, ids, context=None):
""" add the anonymous user in the user of the session """
for session in self.browse(cr, uid, ids, context=context):
users_infos = super(im_chat_session, self).users_infos(cr, uid, ids, context=context)
if session.anonymous_name:
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)
def cron_remove_empty_session(self, cr, uid, context=None):
groups = self.pool['im_chat.message'].read_group(cr, uid, [], ['to_id'], ['to_id'], context=context)
@api.model
def cron_remove_empty_session(self):
groups = self.env['im_chat.message'].read_group([], ['to_id'], ['to_id'])
not_empty_session_ids = [group['to_id'][0] for group in groups]
empty_session_ids = self.search(cr, uid, [('id', 'not in', not_empty_session_ids), ('channel_id', '!=', False)], context=context)
self.unlink(cr, uid, empty_session_ids, context=context)
empty_sessions = self.search([('id', 'not in', not_empty_session_ids), ('channel_id', '!=', False)])
empty_sessions.unlink()
+177 -249
View File
@@ -1,311 +1,239 @@
# -*- coding: utf-8 -*-
import openerp
import openerp.addons.im_chat.im_chat
import random
import re
from openerp.osv import osv, fields
from openerp import api, models, tools
from openerp import SUPERUSER_ID
from openerp import api, fields, models
from openerp import tools
class im_livechat_channel(osv.Model):
class ImLivechatChannel(models.Model):
""" Livechat Channel
Define a communication channel, which can be accessed with 'script_external' (script tag to put on
external website), 'script_internal' (code to be integrated with odoo website) or via 'web_page' link.
It provides rating tools, and access rules for anonymous people.
"""
_name = 'im_livechat.channel'
_description = 'Livechat Channel'
def _get_default_image(self, cr, uid, context=None):
def _default_image(self):
image_path = openerp.modules.get_module_resource('im_livechat', 'static/src/img', 'default.png')
return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
def _get_image(self, cr, uid, ids, name, args, context=None):
result = dict.fromkeys(ids, False)
for obj in self.browse(cr, uid, ids, context=context):
result[obj.id] = tools.image_get_resized_images(obj.image)
return result
def _set_image(self, cr, uid, id, name, value, args, context=None):
return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
def _are_you_inside(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = False
for user in record.user_ids:
if user.id == uid:
res[record.id] = True
break
return res
def _default_user_ids(self):
return [(6, 0, [self._uid])]
def _script_external(self, cr, uid, ids, name, arg, context=None):
# attribute fields
name = fields.Char('Name', required=True, help="The name of the channel")
button_text = fields.Char('Text of the Button', default='Have a Question? Chat with us.',
help="Default text displayed on the Livechat Support Button")
default_message = fields.Char('Welcome Message', default='How may I help you?',
help="This is an automated 'welcome' message that your visitor will see when they initiate a new chat session.")
input_placeholder = fields.Char('Chat Input Placeholder')
# computed fields
web_page = fields.Char('Web Page', compute='_compute_web_page_link', store=False, readonly=True,
help="URL to a static page where you client can discuss with the operator of the channel.")
are_you_inside = fields.Boolean(string='Are you inside the matrix?',
compute='_are_you_inside', store=False, readonly=True)
script_internal = fields.Text('Script (internal)', compute='_compute_script_internal', store=False, readonly=True)
script_external = fields.Text('Script (external)', compute='_compute_script_external', store=False, readonly=True)
nbr_session = fields.Integer('Number of session', compute='_compute_nbr_session', store=False, readonly=True)
rating_percentage_satisfaction = fields.Integer('% Happy', compute='_compute_percentage_satisfaction', store=False, default=-1)
# images fields
image = fields.Binary('Image', default=_default_image,
help="This field holds the image used as photo for the group, limited to 1024x1024px.")
image_small = fields.Binary('Thumbnail', compute='_compute_image', store=True,
help="Small-sized photo of the group. It is automatically "\
"resized as a 64x64px image, with aspect ratio preserved. "\
"Use this field anywhere a small image is required.")
image_medium = fields.Binary('Medium', compute='_compute_image', store=True,
help="Medium-sized photo of the group. It is automatically "\
"resized as a 128x128px image, with aspect ratio preserved. "\
"Use this field in form views or some kanban views.")
# relationnal fields
user_ids = fields.Many2many('res.users', 'im_livechat_channel_im_user', 'channel_id', 'user_id', string='Operators', default=_default_user_ids)
session_ids = fields.One2many('im_chat.session', 'channel_id', 'Sessions')
rule_ids = fields.One2many('im_livechat.channel.rule', 'channel_id', 'Rules')
@api.one
def _are_you_inside(self):
self.are_you_inside = bool(self.env.uid in [u.id for u in self.user_ids])
@api.one
@api.depends('image')
def _compute_image(self):
if self.image:
self.image_small = tools.image_resize_image_small(self.image)
self.image_medium = tools.image_resize_image_medium(self.image)
else:
self.image_small = False
self.image_medium = False
@api.multi
def _compute_script_internal(self):
view = self.env['ir.model.data'].get_object('im_livechat', 'internal_loader')
values = {
"url": self.pool.get('ir.config_parameter').get_param(cr, openerp.SUPERUSER_ID, 'web.base.url'),
"dbname":cr.dbname
"url": self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
"dbname": self._cr.dbname,
}
res = {}
for record in self.browse(cr, uid, ids, context=context):
for record in self:
values["channel"] = record.id
res[record.id] = self.pool['ir.ui.view'].render(cr, uid, 'im_livechat.external_loader', values, context=context)
return res
record.script_internal = view.render(values)
def _script_internal(self, cr, uid, ids, name, arg, context=None):
@api.multi
def _compute_script_external(self):
view = self.env['ir.model.data'].get_object('im_livechat', 'external_loader')
values = {
"url": self.pool.get('ir.config_parameter').get_param(cr, openerp.SUPERUSER_ID, 'web.base.url'),
"dbname":cr.dbname
"url": self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
"dbname": self._cr.dbname,
}
res = {}
for record in self.browse(cr, uid, ids, context=context):
for record in self:
values["channel"] = record.id
res[record.id] = self.pool['ir.ui.view'].render(cr, uid, 'im_livechat.internal_loader', values, context=context)
return res
record.script_external = view.render(values)
def _web_page(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = self.pool.get('ir.config_parameter').get_param(cr, openerp.SUPERUSER_ID, 'web.base.url') + \
"/im_livechat/support/%s/%i" % (cr.dbname, record.id)
return res
@api.multi
def _compute_web_page_link(self):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for record in self:
record.web_page = "%s/im_livechat/support/%s/%i" % (base_url, self._cr.dbname, record.id)
def _compute_nbr_session(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = len(record.session_ids)
return res
@api.multi
@api.depends('session_ids')
def _compute_nbr_session(self):
for record in self:
record.nbr_session = len(record.session_ids)
# 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):
@api.multi
@api.depends('session_ids.rating_ids')
def _compute_percentage_satisfaction(self):
for record in self:
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
record.rating_percentage_satisfaction = ((happy*100) / total) if happy > 0 else 0
else:
res[record.id] = -1
return res
record.rating_percentage_satisfaction = -1
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')]
# --------------------------
# Action Methods
# --------------------------
@api.multi
def action_join(self):
self.ensure_one()
return self.write({'user_ids': [(4, self._uid)]})
@api.multi
def action_quit(self):
self.ensure_one()
return self.write({'user_ids': [(3, self._uid)]})
@api.multi
def action_view_rating(self):
""" Action to display the rating relative to the channel, so all rating of the
sessions of the current channel
:returns : the ir.action 'action_view_rating' with the correct domain
"""
self.ensure_one()
action = self.env['ir.actions.act_window'].for_xml_id('rating', 'action_view_rating')
action['domain'] = [('res_id', 'in', [s.id for s in self.session_ids]), ('res_model', '=', 'im_chat.session')]
return action
_columns = {
'name': fields.char(string="Channel Name", size=200, required=True),
'user_ids': fields.many2many('res.users', 'im_livechat_channel_im_user', 'channel_id', 'user_id', string="Users"),
'are_you_inside': fields.function(_are_you_inside, type='boolean', string='Are you inside the matrix?', store=False),
'script_internal': fields.function(_script_internal, type='text', string='Script (internal)', store=False),
'script_external': fields.function(_script_external, type='text', string='Script (external)', store=False),
'web_page': fields.function(_web_page, type='char', string='Web Page', store=False),
'button_text': fields.char(string="Text of the Button"),
'input_placeholder': fields.char(string="Chat Input Placeholder"),
'default_message': fields.char(string="Welcome Message", help="This is an automated 'welcome' message that your visitor will see when they initiate a new chat session."),
# image: all image fields are base64 encoded and PIL-supported
'image': fields.binary("Photo",
help="This field holds the image used as photo for the group, limited to 1024x1024px."),
'image_medium': fields.function(_get_image, fnct_inv=_set_image,
string="Medium-sized photo", type="binary", multi="_get_image",
store={
'im_livechat.channel': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
},
help="Medium-sized photo of the group. It is automatically "\
"resized as a 128x128px image, with aspect ratio preserved. "\
"Use this field in form views or some kanban views."),
'image_small': fields.function(_get_image, fnct_inv=_set_image,
string="Small-sized photo", type="binary", multi="_get_image",
store={
'im_livechat.channel': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
},
help="Small-sized photo of the group. It is automatically "\
"resized as a 64x64px image, with aspect ratio preserved. "\
"Use this field anywhere a small image is required."),
'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),
}
# --------------------------
# Channel Methods
# --------------------------
@api.multi
def get_available_users(self):
""" get available user of a given channel
:retuns : return the res.users having their im_status online
"""
self.ensure_one()
return self.sudo().user_ids.filtered(lambda user: user.im_status == 'online')
def _default_user_ids(self, cr, uid, context=None):
return [(6, 0, [uid])]
_defaults = {
'button_text': "Have a Question? Chat with us.",
'input_placeholder': "How may I help you?",
'default_message': '',
'user_ids': _default_user_ids,
'image': _get_default_image,
}
def get_available_users(self, cr, uid, channel_id, context=None):
""" get available user of a given channel """
channel = self.browse(cr, SUPERUSER_ID, channel_id, context=context)
users = []
for user_id in channel.user_ids:
if (user_id.im_status == 'online'):
users.append(user_id)
return users
def get_channel_session(self, cr, uid, channel_id, anonymous_name, context=None):
@api.model
def get_channel_session(self, channel_id, anonymous_name):
""" return a session given a channel : create on with a registered user, or return false otherwise """
# get the avalable user of the channel
users = self.get_available_users(cr, SUPERUSER_ID, channel_id, context=context)
users = self.sudo().browse(channel_id).get_available_users()
if len(users) == 0:
return False
user_id = random.choice(users).id
# user to add to the session
user_to_add = [(4, user_id)]
if uid:
user_to_add.append((4, uid))
if self.env.uid:
user_to_add.append((4, self.env.uid))
# create the session, and add the link with the given channel
Session = self.pool["im_chat.session"]
newid = Session.create(cr, SUPERUSER_ID, {'user_ids': user_to_add, 'channel_id': channel_id, 'anonymous_name' : anonymous_name}, context=context)
return Session.session_info(cr, SUPERUSER_ID, [newid], context=context)
session = self.env["im_chat.session"].sudo().create({'user_ids': user_to_add, 'channel_id': channel_id, 'anonymous_name': anonymous_name})
return session.sudo().session_info()
def test_channel(self, cr, uid, channel, context=None):
if not channel:
return {}
return {
'url': self.browse(cr, uid, channel[0], context=context or {}).web_page,
'type': 'ir.actions.act_url'
}
def get_info_for_chat_src(self, cr, uid, channel, context=None):
url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
chan = self.browse(cr, uid, channel, context=context)
@api.model
def get_channel_infos(self, channel_id):
url = self.env['ir.config_parameter'].get_param('web.base.url')
channel = self.browse(channel_id)
return {
"url": url,
'buttonText': chan.button_text,
'inputPlaceholder': chan.input_placeholder,
'defaultMessage': chan.default_message,
"channelName": chan.name,
'buttonText': channel.button_text,
'inputPlaceholder': channel.input_placeholder,
'defaultMessage': channel.default_message,
"channelName": channel.name,
}
def join(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'user_ids': [(4, uid)]})
return True
def quit(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'user_ids': [(3, uid)]})
return True
class im_livechat_channel_rule(osv.Model):
class ImLivechatChannelRule(models.Model):
""" Channel Rules
Rules defining access to the channel (countries, and url matching). It also provide the 'auto pop'
option to open automatically the conversation.
"""
_name = 'im_livechat.channel.rule'
_description = 'Channel Rules'
_order = 'sequence asc'
_columns = {
'regex_url' : fields.char('URL Regex', help="Regular expression identifying the web page on which the rules will be applied."),
'action' : fields.selection([('display_button', 'Display button'),('auto_popup','Auto-popup'), ('hide_button', 'Hide button')], 'Action', size=32, required=True,
help="Select 'Display button' to simply display the chat button on the pages."\
" Select 'Auto-popup' for to display the button, and automatically open the conversation window."\
" Select 'Hide button' to hide the chat button on the pages."),
'auto_popup_timer' : fields.integer('Auto-popup Timer', help="Delay (in seconds) to automatically open the conversation window. Note: the selected action must be 'Auto-popup', otherwise this parameter will not be taken into account."),
'channel_id': fields.many2one('im_livechat.channel', 'Channel', help="Channel on which the rules applies"),
'country_ids': fields.many2many('res.country', 'im_livechat_channel_country_rel', 'channel_id', 'country_id', 'Countries', help="Countries where this rule apply. If you set select 'Belgium' and 'France' and you set the action to 'Hide Button', people from these two countries won't see the support button for the specified URL. (This feature requires GeoIP installed on your server.)"),
'sequence' : fields.integer('Matching Order', help="Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen.")
}
_defaults = {
'auto_popup_timer': 0,
'action' : 'display_button',
'sequence' : 10,
}
regex_url = fields.Char('URL Regex',
help="Regular expression identifying the web page on which the rules will be applied.")
action = fields.Selection([('display_button', 'Display the button'), ('auto_popup', 'Auto popup'), ('hide_button', 'Hide the button')],
string='Action', required=True, default='display_button',
help="* Select 'Display the button' to simply display the chat button on the pages.\n"\
"* Select 'Auto popup' for to display the button, and automatically open the conversation window.\n"\
"* Select 'Hide the button' to hide the chat button on the pages.")
auto_popup_timer = fields.Integer('Auto popup timer', default=0,
help="Delay (in seconds) to automatically open the converssation window. Note : the selected action must be 'Auto popup', otherwise this parameter will not be take into account.")
channel_id = fields.Many2one('im_livechat.channel', 'Channel',
help="The channel of the rule")
country_ids = fields.Many2many('res.country', 'im_livechat_channel_country_rel', 'channel_id', 'country_id', 'Country',
help="The actual rule will match only for this country. So if you set select 'Belgium' and 'France' and you set the action to 'Hide Buttun', this 2 country will not be see the support button for the specified URL. This feature requires GeoIP installed on your server.")
sequence = fields.Integer('Matching order', default=10,
help="Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen.")
_order = "sequence asc"
def match_rule(self, cr, uid, channel_id, url, country_id=False, context=None):
""" determine if a rule of the given channel match with the given url """
def _match(rule_ids):
for rule in self.browse(cr, uid, rule_ids, context=context):
def match_rule(self, channel_id, url, country_id=False):
""" determine if a rule of the given channel match with the given url
:param channel_id : the identifier of the channel_id
:param url : the url to match with a rule
:param country_id : the identifier of the country
:returns the rule that match the given condition. False otherwise.
:rtype : im_livechat.channel.rule
"""
def _match(rules):
for rule in rules:
if re.search(rule.regex_url, url):
return rule
return False
# first, search the country specific rules (the first match is returned)
if country_id: # don't include the country in the research if geoIP is not installed
domain = [('country_ids', 'in', [country_id]), ('channel_id', '=', channel_id)]
rule_ids = self.search(cr, uid, domain, context=context)
rule = _match(rule_ids)
rule = _match(self.search(domain))
if rule:
return rule
# second, fallback on the rules without country
domain = [('country_ids', '=', False), ('channel_id', '=', channel_id)]
rule_ids = self.search(cr, uid, domain, context=context)
return _match(rule_ids)
class im_chat_session(osv.Model):
_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 """
result = {}
sessions = self.browse(cr, uid, ids, context=context)
for session in sessions:
names = []
for user in session.user_ids:
names.append(user.name)
if session.anonymous_name:
names.append(session.anonymous_name)
result[session.id] = ', '.join(names)
return result
_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"),
}
def is_in_session(self, cr, uid, uuid, user_id, context=None):
""" return if the given user_id is in the session """
sids = self.search(cr, uid, [('uuid', '=', uuid)], context=context, limit=1)
for session in self.browse(cr, uid, sids, context=context):
if session.anonymous_name and user_id == openerp.SUPERUSER_ID:
return True
else:
return super(im_chat_session, self).is_in_session(cr, uid, uuid, user_id, context=context)
return False
def users_infos(self, cr, uid, ids, context=None):
""" add the anonymous user in the user of the session """
for session in self.browse(cr, uid, ids, context=context):
users_infos = super(im_chat_session, self).users_infos(cr, uid, ids, context=context)
if session.anonymous_name:
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)
def cron_remove_empty_session(self, cr, uid, context=None):
groups = self.pool['im_chat.message'].read_group(cr, uid, [], ['to_id'], ['to_id'], context=context)
not_empty_session_ids = [group['to_id'][0] for group in groups]
empty_session_ids = self.search(cr, uid, [('id', 'not in', not_empty_session_ids), ('channel_id', '!=', False)], context=context)
self.unlink(cr, uid, empty_session_ids, context=context)
class Rating(models.Model):
_inherit = "rating.rating"
@api.one
@api.depends('res_model', 'res_id')
def _compute_res_name(self):
# cannot change the rec_name of session since it is use to create the bus channel
# so, need to override this method to set the same alternative rec_name as in reporting
if self.res_model == 'im_chat.session':
current_object = self.env[self.res_model].sudo().browse(self.res_id)
self.res_name = ('%s / %s') % (current_object.channel_id.name, current_object.id)
else:
super(Rating, self)._compute_res_name()
return _match(self.search(domain))
+2 -1
View File
@@ -1 +1,2 @@
import im_livechat_report
# -*- coding: utf-8 -*-
import im_livechat_report
+20 -19
View File
@@ -1,28 +1,29 @@
from openerp.osv import fields,osv
from openerp import tools
# -*- coding: utf-8 -*-
from openerp import api, fields, models, tools
class im_livechat_report(osv.Model):
class im_livechat_report(models.Model):
""" Livechat Support Report """
_name = "im_livechat.report"
_auto = False
_description = "Livechat Support Report"
_columns = {
'session_name': fields.char('Session Name', readonly=True),
'uuid': fields.char('UUID', 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"),
'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, session_name'
_auto = False
session_name = fields.Char('Session Name', readonly=True)
uuid = fields.Char('UUID', 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")
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)
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 !
@@ -1,8 +1,8 @@
<?xml version="1.0"?>
<openerp>
<odoo>
<data>
<record model="ir.ui.view" id="view_livechat_support_session_pivot">
<record id="im_livechat_report_view_pivot" model="ir.ui.view">
<field name="name">im_livechat.report.pivot</field>
<field name="model">im_livechat.report</field>
<field name="arch" type="xml">
@@ -15,7 +15,7 @@
</field>
</record>
<record model="ir.ui.view" id="view_livechat_support_session_graph">
<record id="im_livechat_report_view_graph" model="ir.ui.view">
<field name="name">im_livechat.report.graph</field>
<field name="model">im_livechat.report</field>
<field name="arch" type="xml">
@@ -26,7 +26,7 @@
</field>
</record>
<record id="im_livechat_session_search" model="ir.ui.view">
<record id="im_livechat_report_view_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">
@@ -49,7 +49,7 @@
</field>
</record>
<record id="action_livechat_support_report" model="ir.actions.act_window">
<record id="im_livechat_report_action" 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>
@@ -58,9 +58,18 @@
</record>
<menuitem id="menu_reporting_livechat" name="Report" parent="menu_livechat_root" sequence="50" groups="group_im_livechat_manager"/>
<menuitem
id="menu_reporting_livechat"
name="Report"
parent="menu_livechat_root"
sequence="50"
groups="im_livechat_group_manager"/>
<menuitem name="Support Statistics" parent="menu_reporting_livechat" id="livechat_support_report_session" action="action_livechat_support_report"/>
<menuitem
id="livechat_support_report_session"
name="Support Statistics"
parent="menu_reporting_livechat"
action="im_livechat_report_action"/>
</data>
</openerp>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0"?>
<openerp>
<odoo>
<data>
<record id="module_category_im_livechat" model="ir.module.category">
@@ -7,24 +7,24 @@
<field name="sequence" eval="20" />
</record>
<record id="group_im_livechat" model="res.groups">
<record id="im_livechat_group_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_category_im_livechat"/>
<field name="comment">The user will be able to join support channels.</field>
</record>
<record id="group_im_livechat_manager" model="res.groups">
<record id="im_livechat_group_manager" model="res.groups">
<field name="name">Manager</field>
<field name="comment">The user will be able to delete support channels.</field>
<field name="category_id" ref="module_category_im_livechat"/>
<field name="implied_ids" eval="[(4, ref('im_livechat.group_im_livechat'))]"/>
<field name="implied_ids" eval="[(4, ref('im_livechat.im_livechat_group_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<record id="message_rule_1" model="ir.rule">
<record id="im_chat_message_rule_manager" model="ir.rule">
<field name="name">Live Support Managers can read messages from live support</field>
<field name="model_id" ref="im_chat.model_im_chat_message"/>
<field name="groups" eval="[(6,0,[ref('im_livechat.group_im_livechat_manager')])]"/>
<field name="groups" eval="[(6,0,[ref('im_livechat.im_livechat_group_manager')])]"/>
<field name="domain_force">[('to_id.channel_id', '!=', None)]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
@@ -32,10 +32,10 @@
<field name="perm_create" eval="0"/>
</record>
<record id="session_rule_1" model="ir.rule">
<record id="im_chat_session_rule_manager" 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="groups" eval="[(6,0,[ref('im_livechat.im_livechat_group_manager')])]"/>
<field name="domain_force">[('to_id.channel_id', '!=', None)]</field>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="0"/>
@@ -44,4 +44,4 @@
</record>
</data>
</openerp>
</odoo>
@@ -1,8 +1,8 @@
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
access_ls_chann_rule1,im_livechat.channel.rule,model_im_livechat_channel_rule,,1,0,0,0
access_ls_chann_rule2,im_livechat.channel.rule,model_im_livechat_channel_rule,group_im_livechat,1,1,1,0
access_ls_chann_rule3,im_livechat.channel.rule,model_im_livechat_channel_rule,group_im_livechat_manager,1,1,1,1
access_livechat_channel,im_livechat.channel,model_im_livechat_channel,,1,0,0,0
access_livechat_channel_user,im_livechat.channel,model_im_livechat_channel,im_livechat_group_user,1,1,1,0
access_livechat_channel_manager,im_livechat.channel,model_im_livechat_channel,im_livechat_group_manager,1,1,1,1
access_livechat_support_report,im_livechat.report,model_im_livechat_report,im_livechat_group_manager,1,1,1,1
access_livechat_channel_rule,im_livechat.channel.rule,model_im_livechat_channel_rule,,1,0,0,0
access_livechat_channel_rule_user,im_livechat.channel.rule,model_im_livechat_channel_rule,im_livechat_group_user,1,1,1,0
access_livechat_channel_rule_manager,im_livechat.channel.rule,model_im_livechat_channel_rule,im_livechat_group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ls_chann1 access_livechat_channel im_livechat.channel model_im_livechat_channel 1 0 0 0
3 access_ls_chann2 access_livechat_channel_user im_livechat.channel model_im_livechat_channel group_im_livechat im_livechat_group_user 1 1 1 0
4 access_ls_chann3 access_livechat_channel_manager im_livechat.channel model_im_livechat_channel group_im_livechat_manager im_livechat_group_manager 1 1 1 1
5 access_livechat_support_report im_livechat.report model_im_livechat_report group_im_livechat_manager im_livechat_group_manager 1 1 1 1
6 access_ls_chann_rule1 access_livechat_channel_rule im_livechat.channel.rule model_im_livechat_channel_rule 1 0 0 0
7 access_ls_chann_rule2 access_livechat_channel_rule_user im_livechat.channel.rule model_im_livechat_channel_rule group_im_livechat im_livechat_group_user 1 1 1 0
8 access_ls_chann_rule3 access_livechat_channel_rule_manager im_livechat.channel.rule model_im_livechat_channel_rule group_im_livechat_manager im_livechat_group_user 1 1 1 1
@@ -259,10 +259,10 @@ var Feedback = Widget.extend({
_send_feedback: function(close){
var self = this;
var uuid = this.conversation.get('session').uuid;
return user_session.rpc("/rating/livechat/feedback", {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) {
return user_session.rpc('/rating/livechat/feedback', {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) {
if(close){
self.trigger("feedback_sent"); // will close the conversation
self.conversation.send_message(_.str.sprintf(_t("I rated you with :rating_%d"), self.rating), "message");
self.conversation.message_send(_.str.sprintf(_t("I rated you with :rating_%d"), self.rating), "message");
}
});
}
@@ -2,17 +2,7 @@
<openerp>
<data>
<!-- History : form, list and search views -->
<record id="action_session_history" model="ir.actions.act_window">
<field name="name">History</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>
<record id="im_chat_session_search" model="ir.ui.view">
<record id="im_chat_session_view_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">
@@ -30,7 +20,7 @@
</field>
</record>
<record id="im_chat_session_list_view" model="ir.ui.view">
<record id="im_chat_session_view_tree" model="ir.ui.view">
<field name="name">im_chat.session.tree</field>
<field name="model">im_chat.session</field>
<field name="arch" type="xml">
@@ -42,7 +32,7 @@
</field>
</record>
<record id="im_chat_session_form_view" model="ir.ui.view">
<record id="im_chat_session_view_form" model="ir.ui.view">
<field name="name">im_chat.session.form</field>
<field name="model">im_chat.session</field>
<field name="arch" type="xml">
@@ -87,5 +77,27 @@
</field>
</record>
<record id="im_chat_session_action" model="ir.actions.act_window">
<field name="name">History</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>
<record id="im_chat_session_action_from_channel" model="ir.actions.act_window">
<field name="name">Sessions</field>
<field name="res_model">im_chat.session</field>
<field name="view_mode">list,form</field>
<field name="domain">[('channel_id', 'in', [active_id])]</field>
<field name="context">{
'search_default_channel_id': [active_id],
'default_channel_id': active_id,
}</field>
<field name="search_view_id" ref="im_chat_session_view_search"/>
</record>
</data>
</openerp>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<!-- Template rendering the external HTML support page -->
@@ -84,8 +84,8 @@
<!-- Template rendering all the scripts required to execute the Livechat from a page containing Odoo -->
<template id="internal_loader" name="All the scripts to launch the LiveSupport from an internal Web Page">
<!-- css style -->
<link property='stylesheet' t-att-href="'%s/web/css/im_livechat.internal_lib' % (url)" rel="stylesheet"/>
<!-- js of all the required lib (internal and external) -->
<link t-att-href="'%s/web/css/im_livechat.internal_lib' % (url)" rel="stylesheet"/>
<!-- js of all the required lib -->
<script t-att-src="'%s/web/js/im_livechat.internal_lib' % (url)" type="text/javascript" />
<!-- the loader -->
<script t-att-src="'%s/im_livechat/loader/%s/%i' % (url, dbname, channel)" type="text/javascript" />
@@ -114,7 +114,6 @@
<script type="text/javascript" src="/web/static/src/js/framework/widget.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/registry.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/session.js"></script>
<!--<script type="text/javascript" src="/web/static/src/js/framework/model.js"></script>-->
<script type="text/javascript" src="/web/static/src/js/framework/utils.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/core.js"></script>
@@ -169,4 +168,4 @@
</template>
</data>
</openerp>
</odoo>
@@ -2,10 +2,7 @@
<openerp>
<data>
<!--
Views for im_livechat.channel
-->
<record id="action_support_channels" model="ir.actions.act_window">
<record id="im_livechat_channel_action" 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,8 +20,8 @@
</field>
</record>
<record id="support_channel_kanban" model="ir.ui.view">
<field name="name">support_channel.kanban</field>
<record id="im_livechat_channel_view_kanban" model="ir.ui.view">
<field name="name">im_livechat.channel.kanban</field>
<field name="model">im_livechat.channel</field>
<field name="arch" type="xml">
<kanban>
@@ -55,8 +52,8 @@
</i>
</p>
<div style="margin-top:5px;">
<button t-if="record.are_you_inside.raw_value" name="quit" type="object" class="btn btn-default btn-xs">Quit</button>
<button t-if="! record.are_you_inside.raw_value" name="join" type="object" class="btn btn-info btn-xs">Join</button>
<button t-if="record.are_you_inside.raw_value" name="action_quit" type="object" class="btn btn-default btn-xs">Quit</button>
<button t-if="! record.are_you_inside.raw_value" name="action_join" type="object" class="btn btn-info btn-xs">Join</button>
</div>
</div>
</div>
@@ -68,11 +65,11 @@
</field>
</record>
<record id="support_channel_form" model="ir.ui.view">
<field name="name">support_channel.form</field>
<record id="im_livechat_channel_view_form" model="ir.ui.view">
<field name="name">im_livechat.channel.form</field>
<field name="model">im_livechat.channel</field>
<field name="arch" type="xml">
<form string="Support Channels" version="7.0">
<form>
<sheet>
<field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
<div class="oe_title">
@@ -81,14 +78,13 @@
<field name="name" placeholder="e.g. YourWebsite.com"/>
</h1>
<div>
<button type="object" name="join" class="oe_highlight" string="Join Channel" attrs='{"invisible": [["are_you_inside", "=", True]]}'/>
<button type="object" name="quit" string="Leave Channel" attrs='{"invisible": [["are_you_inside", "=", False]]}'/>
<button type="object" name="test_channel" string="Test" attrs='{"invisible": [["web_page", "=", False]]}'/>
<button type="object" name="action_join" class="oe_highlight" string="Join Channel" attrs='{"invisible": [["are_you_inside", "=", True]]}'/>
<button type="object" name="action_quit" string="Leave Channel" attrs='{"invisible": [["are_you_inside", "=", False]]}'/>
<field name="are_you_inside" invisible="1"/>
</div>
</div>
<div class="oe_right oe_button_box" name="buttons">
<button class="oe_inline oe_stat_button" type="action" attrs="{'invisible':[('nbr_session','=', 0)]}" name="%(action_session_channel)d" icon="fa-comments">
<button class="oe_inline oe_stat_button" type="action" attrs="{'invisible':[('nbr_session','=', 0)]}" name="%(im_chat_session_action_from_channel)d" icon="fa-comments">
<field string="Sessions" name="nbr_session" widget="statinfo"/>
</button>
<button name="action_view_rating" attrs="{'invisible':[('rating_percentage_satisfaction','=', -1)]}" class="oe_stat_button" type="object" icon="fa-smile-o">
@@ -146,10 +142,8 @@
<!--
Views for im_livechat_channel_rule
-->
<record id="im_livechat_channel_rule_tree_view" model="ir.ui.view">
<!-- im_livechat.channel.rule -->
<record id="im_livechat_channel_rule_view_tree" model="ir.ui.view">
<field name="name">im.livechat.channel.rule.tree</field>
<field name="model">im_livechat.channel.rule</field>
<field name="arch" type="xml">
@@ -162,7 +156,7 @@
</field>
</record>
<record id="im_livechat_channel_rule_form_view" model="ir.ui.view">
<record id="im_livechat_channel_rule_view_form" model="ir.ui.view">
<field name="name">im_livechat.channel.rule.form</field>
<field name="model">im_livechat.channel.rule</field>
<field name="type">form</field>
@@ -185,11 +179,27 @@
<!-- Menu items -->
<menuitem id="menu_livechat_root" groups="group_im_livechat" name="Live Chat" sequence="34"/>
<menuitem
id="menu_livechat_root"
name="Live Chat"
groups="im_livechat_group_user"
sequence="34"/>
<menuitem name="Channels" parent="menu_livechat_root" id="support_channels" action="action_support_channels" groups="group_im_livechat" sequence="5"/>
<menuitem
id="support_channels"
name="Channels"
parent="menu_livechat_root"
action="im_livechat_channel_action"
groups="im_livechat_group_user"
sequence="5"/>
<menuitem name="History" parent="menu_livechat_root" id="session_history" action="action_session_history" groups="group_im_livechat" sequence="10"/>
<menuitem
id="session_history"
name="History"
parent="menu_livechat_root"
action="im_chat_session_action"
groups="im_livechat_group_user"
sequence="10"/>
</data>
</openerp>
+1
View File
@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
import controllers
import models
+1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
{
'name': 'Website Live Support',
'category': 'Website',
@@ -1 +1,2 @@
# -*- coding: utf-8 -*-
import main
+2 -2
View File
@@ -4,12 +4,12 @@ from openerp import http
from openerp.http import request
class website_rating_livechat(http.Controller):
class WebsiteLivechat(http.Controller):
@http.route('/livechat/', type='http', auth="public", website=True)
def channel_list(self, **kw):
# display the list of the channel
channels = request.env['im_livechat.channel'].sudo().search([('website_published','=', True)])
channels = request.env['im_livechat.channel'].search([('website_published', '=', True)])
values = {
'channels' : channels
}
+2 -1
View File
@@ -1,2 +1,3 @@
import website
# -*- coding: utf-8 -*-
import im_livechat
import website
@@ -3,7 +3,8 @@ from openerp import api, models, fields
from openerp.addons.website.models.website import slug
class im_livechat_channel(models.Model):
class ImLivechatChannel(models.Model):
_name = 'im_livechat.channel'
_inherit = ['im_livechat.channel', 'website.published.mixin']
+9 -8
View File
@@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
from openerp.osv import osv
from openerp import api, fields, models, SUPERUSER_ID
from openerp import api, fields, models
from openerp.http import request
class website(models.Model):
class Website(models.Model):
_inherit = "website"
channel_id = fields.Many2one('im_livechat.channel', string='Live Chat Channel')
class website_config_settings(models.TransientModel):
class WebsiteConfigSettings(models.TransientModel):
_inherit = 'website.config.settings'
@@ -18,15 +18,16 @@ class website_config_settings(models.TransientModel):
class ir_ui_view(osv.Model):
class IrUiView(models.Model):
_inherit = "ir.ui.view"
def _prepare_qcontext(self, cr, uid, context=None):
qcontext = super(ir_ui_view, self)._prepare_qcontext(cr, uid, context=context)
@api.model
def _prepare_qcontext(self):
qcontext = super(IrUiView, self)._prepare_qcontext()
if request and getattr(request, 'website_enabled', False):
if request.website.channel_id:
qcontext['website_livechat_url'] = self.pool.get('ir.config_parameter').get_param(cr, SUPERUSER_ID, 'web.base.url')
qcontext['website_livechat_dbname'] = cr.dbname
qcontext['website_livechat_url'] = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
qcontext['website_livechat_dbname'] = self._cr.dbname
qcontext['website_livechat_channel'] = request.website.channel_id.id
return qcontext
@@ -2,7 +2,7 @@
<odoo>
<data>
<record model="ir.rule" id="website_channel_public">
<record id="im_livechat_channel_rule_public" model="ir.rule">
<field name="name">website_livechat.channel.public</field>
<field name="model_id" ref="im_livechat.model_im_livechat_channel"/>
<field name="domain_force">[('website_published', '=', True)]</field>
@@ -3,9 +3,9 @@
<data>
<record id="im_livechat_channel_form_view" model="ir.ui.view">
<field name="name">im_livechat.channel.form.with.website_published</field>
<field name="name">im_livechat.channel.form.inherit.website_livechat</field>
<field name="model">im_livechat.channel</field>
<field name="inherit_id" ref="im_livechat.support_channel_form"/>
<field name="inherit_id" ref="im_livechat.im_livechat_channel_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='buttons']" position="inside">
<button class="oe_inline oe_stat_button" name="open_website_url"
@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<record id="channel_website" model="im_livechat.channel">
<record id="im_livechat_channel_data_website" model="im_livechat.channel">
<field name="name">YourWebsiteWithOdoo.com</field>
<field name="default_message">Hello, how may I help you?</field>
</record>
<record id="website.default_website" model="website">
<field name="channel_id" ref="website_livechat.channel_website"></field>
<field name="channel_id" ref="website_livechat.im_livechat_channel_data_website"></field>
</record>
</data>
</openerp>
</odoo>