MERGE] odoo bus, im_chat and im_livechat

Add a generic bus for instant communication based on postgres LISTEN/NOTIFY and HTTP comet.
Both threaded and gevent greenlet mode are supported. Chat should now work on every platform.

im_chat improvements
- proper support for multiple windows
- present, away and offline status
- improved data model for multi user chat session

im_livechat improvements
- standard css js assets are now used
- qweb templates are now used instead of jinnja
This commit is contained in:
Antony Lesuisse 2014-06-29 03:46:29 +02:00
parent 25d0a3ecb1
commit c60798d3b0
70 changed files with 1860 additions and 15635 deletions

1
addons/bus/__init__.py Normal file
View File

@ -0,0 +1 @@
import bus

13
addons/bus/__openerp__.py Normal file
View File

@ -0,0 +1,13 @@
{
'name' : 'IM Bus',
'version': '1.0',
'author': 'OpenERP SA',
'category': 'Hidden',
'complexity': 'easy',
'description': "Instant Messaging Bus allow you to send messages to users, in live.",
'depends': ['base', 'web'],
'data': [
'views/bus.xml',
'security/ir.model.access.csv',
],
}

194
addons/bus/bus.py Normal file
View File

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
import datetime
import json
import logging
import select
import threading
import time
import random
import simplejson
import openerp
from openerp.osv import osv, fields
from openerp.http import request
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
_logger = logging.getLogger(__name__)
TIMEOUT = 50
#----------------------------------------------------------
# Bus
#----------------------------------------------------------
def json_dump(v):
return simplejson.dumps(v, separators=(',', ':'))
def hashable(key):
if isinstance(key, list):
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):
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)
def sendmany(self, cr, uid, notifications):
channels = set()
for channel, message in notifications:
channels.add(channel)
values = {
"channel" : json_dump(channel),
"message" : json_dump(message)
}
cr.commit()
self.pool['bus.bus'].create(cr, openerp.SUPERUSER_ID, values)
if random.random() < 0.01:
self.gc(cr, uid)
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]])
def poll(self, cr, uid, 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)]
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]
class ImDispatch(object):
def __init__(self):
self.channels = {}
def poll(self, dbname, channels, last, timeout=TIMEOUT):
# Dont hang ctrl-c for a poll request, we need to bypass private
# attribute access because we dont know before starting the thread that
# it will handle a longpolling request
if not openerp.evented:
current = threading.current_thread()
current._Thread__daemonic = True
# rename the thread to avoid tests waiting for a longpolling
current.setName("openerp.longpolling.request.%s" % current.ident)
registry = openerp.registry(dbname)
# immediatly returns if past notifications exist
with registry.cursor() as cr:
notifications = registry['bus.bus'].poll(cr, openerp.SUPERUSER_ID, channels, last)
# or wait for future ones
if not notifications:
event = self.Event()
for c in channels:
self.channels.setdefault(hashable(c), []).append(event)
try:
event.wait(timeout=timeout)
with registry.cursor() as cr:
notifications = registry['bus.bus'].poll(cr, openerp.SUPERUSER_ID, channels, last)
except Exception:
# timeout
pass
return notifications
def loop(self):
""" Dispatch postgres notifications to the relevant polling threads/greenlets """
_logger.info("Bus.loop listen imbus on db postgres")
with openerp.sql_db.db_connect('postgres').cursor() as cr:
conn = cr._cnx
cr.execute("listen imbus")
cr.commit();
while True:
if select.select([conn], [], [], TIMEOUT) == ([],[],[]):
pass
else:
conn.poll()
channels = []
while conn.notifies:
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()
def run(self):
while True:
try:
self.loop()
except Exception, e:
_logger.exception("Bus.loop error, sleep and retry")
time.sleep(TIMEOUT)
def start(self):
if openerp.evented:
# gevent mode
import gevent
self.Event = gevent.event.Event
gevent.spawn(self.run)
elif openerp.multi_process:
# disabled in prefork mode
return
else:
# threaded mode
self.Event = threading.Event
t = threading.Thread(name="%s.Bus" % __name__, target=self.run)
t.daemon = True
t.start()
return self
dispatch = ImDispatch().start()
#----------------------------------------------------------
# Controller
#----------------------------------------------------------
class Controller(openerp.http.Controller):
""" Examples:
openerp.jsonRpc('/longpolling/poll','call',{"channels":["c1"],last:0}).then(function(r){console.log(r)});
openerp.jsonRpc('/longpolling/send','call',{"channel":"c1","message":"m1"});
openerp.jsonRpc('/longpolling/send','call',{"channel":"c2","message":"m2"});
"""
@openerp.http.route('/longpolling/send', type="json", auth="public")
def send(self, channel, message):
if not isinstance(channel, basestring):
raise Exception("bus.Bus only string channels are allowed.")
registry, cr, uid, context = request.registry, request.cr, request.session.uid, request.context
return registry['bus.bus'].sendone(cr, uid, channel, message)
# override to add channels
def _poll(self, dbname, channels, last, options):
request.cr.close()
request._cr = None
return dispatch.poll(dbname, channels, last)
@openerp.http.route('/longpolling/poll', type="json", auth="public")
def poll(self, channels, last, options=None):
if options is None:
options = {}
if not dispatch:
raise Exception("bus.Bus unavailable")
if [c for c in channels if not isinstance(c, basestring)]:
print channels
raise Exception("bus.Bus only string channels are allowed.")
return self._poll(request.db, channels, last, options)
# vim:et:

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_bus_bus bus.bus public model_bus_bus 0 0 0 0

View File

@ -0,0 +1,49 @@
(function() {
var bus = openerp.bus = {};
bus.ERROR_DELAY = 30000;
bus.Bus = openerp.Widget.extend({
init: function(){
this._super();
this.options = {};
this.activated = false;
this.channels = [];
this.last = 0;
},
start_polling: function(){
if(!this.activated){
this.poll();
}
},
poll: function() {
var self = this;
self.activated = true;
var data = {'channels': self.channels, 'last': self.last, 'options' : self.options};
openerp.jsonRpc('/longpolling/poll', 'call', data).then(function(result) {
_.each(result, _.bind(self.on_notification, self));
self.poll();
}, function(unused, e) {
setTimeout(_.bind(self.poll, self), bus.ERROR_DELAY);
});
},
on_notification: function(notification) {
if (notification.id > this.last) {
this.last = notification.id;
}
this.trigger("notification", [notification.channel, notification.message]);
},
add_channel: function(channel){
if(!_.contains(this.channels, channel)){
this.channels.push(channel);
}
},
delete_channel: function(channel){
this.channels = _.without(this.channels, channel);
},
});
// singleton
bus.bus = new bus.Bus();
return bus;
})();

12
addons/bus/views/bus.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- vim:fdn=3:
-->
<openerp>
<data>
<template id="assets_backend" name="im assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/bus/static/src/js/bus.js"></script>
</xpath>
</template>
</data>
</openerp>

View File

@ -1,20 +0,0 @@
module.exports = function(grunt) {
grunt.initConfig({
jshint: {
src: ['static/src/js/*.js'],
options: {
sub: true, //[] instead of .
evil: true, //eval
laxbreak: true, //unsafe line breaks
},
},
});
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.registerTask('test', []);
grunt.registerTask('default', ['jshint']);
};

View File

@ -1,2 +0,0 @@
import im

View File

@ -1,362 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import datetime
import json
import logging
import select
import time
import openerp
import openerp.tools.config
import openerp.modules.registry
from openerp import http
from openerp.http import request
from openerp.osv import osv, fields, expression
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
_logger = logging.getLogger(__name__)
def listen_channel(cr, channel_name, handle_message, check_stop=(lambda: False), check_stop_timer=60.):
"""
Begin a loop, listening on a PostgreSQL channel. This method does never terminate by default, you need to provide a check_stop
callback to do so. This method also assume that all notifications will include a message formated using JSON (see the
corresponding notify_channel() method).
:param db_name: database name
:param channel_name: the name of the PostgreSQL channel to listen
:param handle_message: function that will be called when a message is received. It takes one argument, the message
attached to the notification.
:type handle_message: function (one argument)
:param check_stop: function that will be called periodically (see the check_stop_timer argument). If it returns True
this function will stop to watch the channel.
:type check_stop: function (no arguments)
:param check_stop_timer: The maximum amount of time between calls to check_stop_timer (can be shorter if messages
are received).
"""
try:
conn = cr._cnx
cr.execute("listen " + channel_name + ";")
cr.commit();
stopping = False
while not stopping:
if check_stop():
stopping = True
break
if select.select([conn], [], [], check_stop_timer) == ([],[],[]):
pass
else:
conn.poll()
while conn.notifies:
message = json.loads(conn.notifies.pop().payload)
handle_message(message)
finally:
try:
cr.execute("unlisten " + channel_name + ";")
cr.commit()
except:
pass # can't do anything if that fails
def notify_channel(cr, channel_name, message):
"""
Send a message through a PostgreSQL channel. The message will be formatted using JSON. This method will
commit the given transaction because the notify command in Postgresql seems to work correctly when executed in
a separate transaction (despite what is written in the documentation).
:param cr: The cursor.
:param channel_name: The name of the PostgreSQL channel.
:param message: The message, must be JSON-compatible data.
"""
cr.commit()
cr.execute("notify " + channel_name + ", %s", [json.dumps(message)])
cr.commit()
POLL_TIMER = 30
DISCONNECTION_TIMER = POLL_TIMER + 5
WATCHER_ERROR_DELAY = 10
class LongPollingController(http.Controller):
@http.route('/longpolling/im/poll', type="json", auth="none")
def poll(self, last=None, users_watch=None, db=None, uid=None, password=None, uuid=None):
assert_uuid(uuid)
if not openerp.evented:
raise Exception("Not usable in a server not running gevent")
from openerp.addons.im.watcher import ImWatcher
if db is not None:
openerp.service.security.check(db, uid, password)
else:
uid = request.session.uid
db = request.session.db
registry = openerp.modules.registry.RegistryManager.get(db)
with registry.cursor() as cr:
registry.get('im.user').im_connect(cr, uid, uuid=uuid, context=request.context)
my_id = registry.get('im.user').get_my_id(cr, uid, uuid, request.context)
num = 0
while True:
with registry.cursor() as cr:
res = registry.get('im.message').get_messages(cr, uid, last, users_watch, uuid=uuid, context=request.context)
if num >= 1 or len(res["res"]) > 0:
return res
last = res["last"]
num += 1
ImWatcher.get_watcher(res["dbname"]).stop(my_id, users_watch or [], POLL_TIMER)
@http.route('/longpolling/im/activated', type="json", auth="none")
def activated(self):
return not not openerp.evented
@http.route('/longpolling/im/gen_uuid', type="json", auth="none")
def gen_uuid(self):
import uuid
return "%s" % uuid.uuid1()
def assert_uuid(uuid):
if not isinstance(uuid, (str, unicode, type(None))) and uuid != False:
raise Exception("%s is not a uuid" % uuid)
class im_message(osv.osv):
_name = 'im.message'
_order = "date desc"
_columns = {
'message': fields.text(string="Message", required=True),
'from_id': fields.many2one("im.user", "From", required= True, ondelete='cascade'),
'session_id': fields.many2one("im.session", "Session", required=True, select=True, ondelete='cascade'),
'to_id': fields.many2many("im.user", "im_message_users", 'message_id', 'user_id', 'To'),
'date': fields.datetime("Date", required=True, select=True),
'technical': fields.boolean("Technical Message"),
}
_defaults = {
'date': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'technical': False,
}
def get_messages(self, cr, uid, last=None, users_watch=None, uuid=None, context=None):
assert_uuid(uuid)
users_watch = users_watch or []
# complex stuff to determine the last message to show
users = self.pool.get("im.user")
my_id = users.get_my_id(cr, uid, uuid, context=context)
c_user = users.browse(cr, openerp.SUPERUSER_ID, my_id, context=context)
if last:
if c_user.im_last_received < last:
users.write(cr, openerp.SUPERUSER_ID, my_id, {'im_last_received': last}, context=context)
else:
last = c_user.im_last_received or -1
# how fun it is to always need to reorder results from read
mess_ids = self.search(cr, openerp.SUPERUSER_ID, ["&", ['id', '>', last], "|", ['from_id', '=', my_id], ['to_id', 'in', [my_id]]], order="id", context=context)
mess = self.read(cr, openerp.SUPERUSER_ID, mess_ids, ["id", "message", "from_id", "session_id", "date", "technical"], context=context)
index = {}
for i in xrange(len(mess)):
index[mess[i]["id"]] = mess[i]
mess = []
for i in mess_ids:
mess.append(index[i])
if len(mess) > 0:
last = mess[-1]["id"]
users_status = users.read(cr, openerp.SUPERUSER_ID, users_watch, ["im_status"], context=context)
return {"res": mess, "last": last, "dbname": cr.dbname, "users_status": users_status}
def post(self, cr, uid, message, to_session_id, technical=False, uuid=None, context=None):
assert_uuid(uuid)
my_id = self.pool.get('im.user').get_my_id(cr, uid, uuid)
session_user_ids = self.pool.get('im.session').get_session_users(cr, uid, to_session_id, context=context).get("user_ids", [])
to_ids = [user_id for user_id in session_user_ids if user_id != my_id]
self.create(cr, openerp.SUPERUSER_ID, {"message": message, 'from_id': my_id,
'to_id': [(6, 0, to_ids)], 'session_id': to_session_id, 'technical': technical}, context=context)
notify_channel(cr, "im_channel", {'type': 'message', 'receivers': [my_id] + to_ids})
return False
class im_session(osv.osv):
_name = 'im.session'
def _calc_name(self, cr, uid, ids, something, something_else, context=None):
res = {}
for obj in self.browse(cr, uid, ids, context=context):
res[obj.id] = ", ".join([x.name for x in obj.user_ids])
return res
_columns = {
'user_ids': fields.many2many('im.user', 'im_session_im_user_rel', 'im_session_id', 'im_user_id', 'Users'),
"name": fields.function(_calc_name, string="Name", type='char'),
}
# Todo: reuse existing sessions if possible
def session_get(self, cr, uid, users_to, uuid=None, context=None):
my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
users = [my_id] + users_to
domain = []
for user_to in users:
domain.append(('user_ids', 'in', [user_to]))
sids = self.search(cr, openerp.SUPERUSER_ID, domain, context=context, limit=1)
session_id = None
for session in self.browse(cr, uid, sids, context=context):
if len(session.user_ids) == len(users):
session_id = session.id
break
if not session_id:
session_id = self.create(cr, openerp.SUPERUSER_ID, {
'user_ids': [(6, 0, users)]
}, context=context)
return self.read(cr, uid, session_id, context=context)
def get_session_users(self, cr, uid, session_id, context=None):
return self.read(cr, openerp.SUPERUSER_ID, session_id, ['user_ids'], context=context)
def add_to_session(self, cr, uid, session_id, user_id, uuid=None, context=None):
my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
session = self.read(cr, uid, session_id, context=context)
if my_id not in session.get("user_ids"):
raise Exception("Not allowed to modify a session when you are not in it.")
self.write(cr, uid, session_id, {"user_ids": [(4, user_id)]}, context=context)
def remove_me_from_session(self, cr, uid, session_id, uuid=None, context=None):
my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
self.write(cr, openerp.SUPERUSER_ID, session_id, {"user_ids": [(3, my_id)]}, context=context)
class im_user(osv.osv):
_name = "im.user"
def _im_status(self, cr, uid, ids, something, something_else, context=None):
res = {}
current = datetime.datetime.now()
delta = datetime.timedelta(0, DISCONNECTION_TIMER)
data = self.read(cr, openerp.SUPERUSER_ID, ids, ["im_last_status_update", "im_last_status"], context=context)
for obj in data:
last_update = datetime.datetime.strptime(obj["im_last_status_update"], DEFAULT_SERVER_DATETIME_FORMAT)
res[obj["id"]] = obj["im_last_status"] and (last_update + delta) > current
return res
def _status_search(self, cr, uid, obj, name, domain, context=None):
current = datetime.datetime.now()
delta = datetime.timedelta(0, DISCONNECTION_TIMER)
field, operator, value = domain[0]
if operator in expression.NEGATIVE_TERM_OPERATORS:
value = not value
if value:
return ['&', ('im_last_status', '=', True), ('im_last_status_update', '>', (current - delta).strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
else:
return ['|', ('im_last_status', '=', False), ('im_last_status_update', '<=', (current - delta).strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
# TODO: Remove fields arg in trunk. Also in im.js.
def search_users(self, cr, uid, text_search, fields, limit, context=None):
my_id = self.get_my_id(cr, uid, None, context)
group_employee = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base', 'group_user')[1]
found = self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", True], ["user_id.groups_id", "in", [group_employee]]],
order="name asc", limit=limit, context=context)
if len(found) < limit:
found += self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", True], ["id", "not in", found]],
order="name asc", limit=limit, context=context)
if len(found) < limit:
found += self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", False], ["id", "not in", found]],
order="name asc", limit=limit-len(found), context=context)
users = self.read(cr,openerp.SUPERUSER_ID, found, ["name", "user_id", "uuid", "im_status"], context=context)
users.sort(key=lambda obj: found.index(obj['id']))
return users
def im_connect(self, cr, uid, uuid=None, context=None):
assert_uuid(uuid)
return self._im_change_status(cr, uid, True, uuid, context)
def im_disconnect(self, cr, uid, uuid=None, context=None):
assert_uuid(uuid)
return self._im_change_status(cr, uid, False, uuid, context)
def _im_change_status(self, cr, uid, new_one, uuid=None, context=None):
assert_uuid(uuid)
id = self.get_my_id(cr, uid, uuid, context=context)
current_status = self.read(cr, openerp.SUPERUSER_ID, id, ["im_status"], context=None)["im_status"]
self.write(cr, openerp.SUPERUSER_ID, id, {"im_last_status": new_one,
"im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
if current_status != new_one:
notify_channel(cr, "im_channel", {'type': 'status', 'user': id})
return True
def get_my_id(self, cr, uid, uuid=None, context=None):
assert_uuid(uuid)
if uuid:
users = self.search(cr, openerp.SUPERUSER_ID, [["uuid", "=", uuid]], context=None)
else:
users = self.search(cr, openerp.SUPERUSER_ID, [["user_id", "=", uid]], context=None)
my_id = users[0] if len(users) >= 1 else False
if not my_id:
my_id = self.create(cr, openerp.SUPERUSER_ID, {"user_id": uid if not uuid else False, "uuid": uuid if uuid else False}, context=context)
return my_id
def assign_name(self, cr, uid, uuid, name, context=None):
assert_uuid(uuid)
id = self.get_my_id(cr, uid, uuid, context=context)
self.write(cr, openerp.SUPERUSER_ID, id, {"assigned_name": name}, context=context)
return True
def _get_name(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = record.assigned_name
if record.user_id:
res[record.id] = record.user_id.name
continue
return res
def get_users(self, cr, uid, ids, context=None):
return self.read(cr,openerp.SUPERUSER_ID, ids, ["name", "im_status", "uuid"], context=context)
_columns = {
'name': fields.function(_get_name, type='char', size=200, string="Name", store=True, readonly=True),
'assigned_name': fields.char(string="Assigned Name", size=200, required=False),
'image': fields.related('user_id', 'image_small', type='binary', string="Image", readonly=True),
'user_id': fields.many2one("res.users", string="User", select=True, ondelete='cascade', oldname='user'),
'uuid': fields.char(string="UUID", size=50, select=True),
'im_last_received': fields.integer(string="Instant Messaging Last Received Message"),
'im_last_status': fields.boolean(strint="Instant Messaging Last Status"),
'im_last_status_update': fields.datetime(string="Instant Messaging Last Status Update"),
'im_status': fields.function(_im_status, string="Instant Messaging Status", type='boolean', fnct_search=_status_search),
}
_defaults = {
'im_last_received': -1,
'im_last_status': False,
'im_last_status_update': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
}
_sql_constraints = [
('user_uniq', 'unique (user_id)', 'Only one chat user per OpenERP user.'),
('uuid_uniq', 'unique (uuid)', 'Chat identifier already used.'),
]
class res_users(osv.osv):
_inherit = "res.users"
def _get_im_user(self, cr, uid, ids, field_name, arg, context=None):
result = dict.fromkeys(ids, False)
for index, im_user in enumerate(self.pool['im.user'].search_read(cr, uid, domain=[('user_id', 'in', ids)], fields=['name', 'user_id'], context=context)):
result[ids[index]] = im_user.get('user_id') and (im_user['user_id'][0], im_user['name']) or False
return result
_columns = {
'im_user_id' : fields.function(_get_im_user, type='many2one', string="IM User", relation="im.user"),
}

View File

@ -1,6 +0,0 @@
{
"devDependencies": {
"grunt": "*",
"grunt-contrib-jshint": "*"
}
}

View File

@ -1,26 +0,0 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="message_rule_1" model="ir.rule">
<field name="name">Can only read messages from a session where user is</field>
<field name="model_id" ref="model_im_message"/>
<field name="groups" eval="[(6,0,[ref('base.group_user')])]"/>
<field name="domain_force">[('session_id.user_ids', 'in', user.im_user_id.id)]</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="0"/>
</record>
<record id="users_rule_1" model="ir.rule">
<field name="name">Can only modify your user</field>
<field name="model_id" ref="model_im_user"/>
<field name="groups" eval="[(6,0,[ref('base.group_user')])]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
</data>
</openerp>

View File

@ -1,4 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_im_message,im.message,model_im_message,,1,0,1,0
access_im_user,im.user,model_im_user,,1,1,1,0
access_im_session,im.session,model_im_session,,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_im_message im.message model_im_message 1 0 1 0
3 access_im_user im.user model_im_user 1 1 1 0
4 access_im_session im.session model_im_session 1 1 1 0

View File

@ -1,176 +0,0 @@
(function() {
"use strict";
var instance = openerp;
openerp.im = {};
var USERS_LIMIT = 20;
var _t = instance.web._t;
var QWeb = instance.web.qweb;
instance.web.UserMenu.include({
do_update: function(){
var self = this;
this.update_promise.then(function() {
im_common.notification = function(message) {
instance.client.do_warn(message);
};
// TODO: allow to use a different host for the chat
im_common.connection = new openerp.Session(self, null, {session_id: openerp.session.session_id});
var im = new instance.im.InstantMessaging(self);
im.appendTo(instance.client.$el);
var button = new instance.im.ImTopButton(this);
button.on("clicked", im, im.switch_display);
button.appendTo(window.$('.oe_systray'));
});
return this._super.apply(this, arguments);
},
});
instance.im.ImTopButton = instance.web.Widget.extend({
template:'ImTopButton',
events: {
"click": "clicked",
},
clicked: function(ev) {
ev.preventDefault();
this.trigger("clicked");
},
});
instance.im.InstantMessaging = instance.web.Widget.extend({
template: "InstantMessaging",
events: {
"keydown .oe_im_searchbox": "input_change",
"keyup .oe_im_searchbox": "input_change",
"change .oe_im_searchbox": "input_change",
},
init: function(parent) {
this._super(parent);
this.shown = false;
this.set("right_offset", 0);
this.set("current_search", "");
this.users = [];
this.c_manager = new im_common.ConversationManager(this);
window.im_conversation_manager = this.c_manager;
this.on("change:right_offset", this.c_manager, _.bind(function() {
this.c_manager.set("right_offset", this.get("right_offset"));
}, this));
this.user_search_dm = new instance.web.DropMisordered();
},
start: function() {
var self = this;
this.$el.css("right", -this.$el.outerWidth());
$(window).scroll(_.bind(this.calc_box, this));
$(window).resize(_.bind(this.calc_box, this));
this.calc_box();
this.on("change:current_search", this, this.search_changed);
return this.c_manager.start_polling().then(function() {
self.c_manager.on("new_conversation", self, function(conv) {
conv.$el.droppable({
drop: function(event, ui) {
self.add_user(conv, ui.draggable.data("user"));
}
});
});
self.search_changed();
});
},
calc_box: function() {
var $topbar = window.$('#oe_main_menu_navbar'); // .oe_topbar is replaced with .navbar of bootstrap3
var top = $topbar.offset().top + $topbar.height();
top = Math.max(top - $(window).scrollTop(), 0);
this.$el.css("top", top);
this.$el.css("bottom", 0);
},
input_change: function() {
this.set("current_search", this.$(".oe_im_searchbox").val());
},
search_changed: function(e) {
var users = new instance.web.Model("im.user");
var self = this;
// TODO: Remove fields arg in trunk. Also in im.js.
return this.user_search_dm.add(users.call("search_users", [this.get("current_search"), ["name", "user_id", "uuid", "im_status"],
USERS_LIMIT], {context:new instance.web.CompoundContext()})).then(function(users) {
var logged_users = _.filter(users, function(u) { return !!u.im_status; });
var non_logged_users = _.filter(users, function(u) { return !u.im_status; });
users = logged_users.concat(non_logged_users);
self.c_manager.add_to_user_cache(users);
self.$(".oe_im_input").val("");
var old_users = self.users;
self.users = [];
_.each(users, function(user) {
var widget = new instance.im.UserWidget(self, self.c_manager.get_user(user.id));
widget.appendTo(self.$(".oe_im_users"));
widget.on("activate_user", self, function(user) {self.c_manager.chat_with_users([user]);});
self.users.push(widget);
});
_.each(old_users, function(user) {
user.destroy();
});
});
},
switch_display: function() {
var fct = _.bind(function(place) {
this.set("right_offset", place + this.$el.outerWidth());
}, this);
var opt = {
step: fct,
};
if (this.shown) {
this.$el.animate({
right: -this.$el.outerWidth(),
}, opt);
} else {
if (! this.c_manager.get_activated()) {
this.do_warn("Instant Messaging is not activated on this server.", "");
return;
}
this.$el.animate({
right: 0,
}, opt);
}
this.shown = ! this.shown;
},
add_user: function(conversation, user) {
conversation.add_user(user);
},
});
instance.im.UserWidget = instance.web.Widget.extend({
"template": "UserWidget",
events: {
"click": "activate_user",
},
init: function(parent, user) {
this._super(parent);
this.user = user;
this.user.add_watcher();
},
start: function() {
this.$el.data("user", this.user);
this.$el.draggable({helper: "clone"});
var change_status = function() {
this.$(".oe_im_user_online").toggle(this.user.get("im_status") === true);
};
this.user.on("change:im_status", this, change_status);
change_status.call(this);
},
activate_user: function() {
this.trigger("activate_user", this.user);
},
destroy: function() {
this.user.remove_watcher();
this._super();
},
});
im_common.technical_messages_handlers.force_kitten = function() {
openerp.webclient.to_kitten();
};
})();

View File

@ -1,562 +0,0 @@
/*
This file must compile in EcmaScript 3 and work in IE7.
Prerequisites to use this module:
- load the im_common.xml qweb template into openerp.qweb
- implement all the stuff defined later
*/
(function() {
function declare($, _, openerp) {
/* jshint es3: true */
"use strict";
var im_common = {};
/*
All of this must be defined to use this module
*/
_.extend(im_common, {
notification: function(message) {
throw new Error("Not implemented");
},
connection: null
});
var _t = openerp._t;
var ERROR_DELAY = 5000;
im_common.ImUser = openerp.Class.extend(openerp.PropertiesMixin, {
init: function(parent, user_rec) {
openerp.PropertiesMixin.init.call(this, parent);
user_rec.image_url = im_common.connection.url('/web/binary/image', {model:'im.user', field: 'image', id: user_rec.id});
this.set(user_rec);
this.set("watcher_count", 0);
this.on("change:watcher_count", this, function() {
if (this.get("watcher_count") === 0)
this.destroy();
});
},
destroy: function() {
this.trigger("destroyed");
openerp.PropertiesMixin.destroy.call(this);
},
add_watcher: function() {
this.set("watcher_count", this.get("watcher_count") + 1);
},
remove_watcher: function() {
this.set("watcher_count", this.get("watcher_count") - 1);
}
});
im_common.ConversationManager = openerp.Class.extend(openerp.PropertiesMixin, {
init: function(parent, options) {
openerp.PropertiesMixin.init.call(this, parent);
this.options = _.clone(options) || {};
_.defaults(this.options, {
inputPlaceholder: _t("Say something..."),
defaultMessage: null,
userName: _t("Anonymous"),
anonymous_mode: false
});
this.set("right_offset", 0);
this.set("bottom_offset", 0);
this.conversations = [];
this.on("change:right_offset", this, this.calc_positions);
this.on("change:bottom_offset", this, this.calc_positions);
this.set("window_focus", true);
this.set("waiting_messages", 0);
this.focus_hdl = _.bind(function() {
this.set("window_focus", true);
}, this);
$(window).bind("focus", this.focus_hdl);
this.blur_hdl = _.bind(function() {
this.set("window_focus", false);
}, this);
$(window).bind("blur", this.blur_hdl);
this.on("change:window_focus", this, this.window_focus_change);
this.window_focus_change();
this.on("change:waiting_messages", this, this.messages_change);
this.messages_change();
this.create_ting();
this.activated = false;
this.users_cache = {};
this.last = null;
this.unload_event_handler = _.bind(this.unload, this);
},
start_polling: function() {
var self = this;
var def = $.when();
var uuid = false;
if (this.options.anonymous_mode) {
uuid = localStorage["oe_livesupport_uuid"] || false;
if (! uuid) {
def = im_common.connection.rpc("/longpolling/im/gen_uuid", {}).then(function(my_uuid) {
uuid = my_uuid;
localStorage["oe_livesupport_uuid"] = uuid;
});
}
def = def.then(function() {
return im_common.connection.model("im.user").call("assign_name", [uuid, self.options.userName]);
});
}
return def.then(function() {
return im_common.connection.model("im.user").call("get_my_id", [uuid]);
}).then(function(my_user_id) {
self.my_id = my_user_id;
return self.ensure_users([self.my_id]);
}).then(function() {
var me = self.users_cache[self.my_id];
delete self.users_cache[self.my_id];
self.me = me;
me.set("name", _t("You"));
return im_common.connection.rpc("/longpolling/im/activated", {}, {shadow: true});
}).then(function(activated) {
if (activated) {
self.activated = true;
$(window).on("unload", self.unload_event_handler);
self.poll();
} else {
return $.Deferred().reject();
}
}, function(a, e) {
e.preventDefault();
});
},
unload: function() {
return im_common.connection.model("im.user").call("im_disconnect", [], {uuid: this.me.get("uuid"), context: {}});
},
ensure_users: function(user_ids) {
var no_cache = {};
_.each(user_ids, function(el) {
if (! this.users_cache[el])
no_cache[el] = el;
}, this);
var self = this;
var def;
if (_.size(no_cache) === 0)
def = $.when();
else
def = im_common.connection.model("im.user").call("get_users", [_.values(no_cache)]).then(function(users) {
self.add_to_user_cache(users);
});
return def.then(function() {
return _.map(user_ids, function(id) { return self.get_user(id); });
});
},
add_to_user_cache: function(user_recs) {
_.each(user_recs, function(user_rec) {
if (! this.users_cache[user_rec.id]) {
var user = new im_common.ImUser(this, user_rec);
this.users_cache[user_rec.id] = user;
user.on("destroyed", this, function() {
delete this.users_cache[user_rec.id];
});
}
}, this);
},
get_user: function(user_id) {
return this.users_cache[user_id];
},
poll: function() {
var self = this;
var user_ids = _.map(this.users_cache, function(el) {
return el.get("id");
});
im_common.connection.rpc("/longpolling/im/poll", {
last: this.last,
users_watch: user_ids,
uuid: self.me.get("uuid")
}, {shadow: true}).then(function(result) {
_.each(result.users_status, function(el) {
if (self.get_user(el.id))
self.get_user(el.id).set(el);
});
self.last = result.last;
self.received_messages(result.res).then(function() {
self.poll();
});
}, function(unused, e) {
e.preventDefault();
setTimeout(_.bind(self.poll, self), ERROR_DELAY);
});
},
get_activated: function() {
return this.activated;
},
create_ting: function() {
if (typeof(Audio) === "undefined") {
this.ting = {play: function() {}};
return;
}
var kitten = jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
this.ting = new Audio(im_common.connection.url(
"/im/static/src/audio/" +
(kitten ? "purr" : "Ting") +
(new Audio().canPlayType("audio/ogg; codecs=vorbis") ? ".ogg": ".mp3")
));
},
window_focus_change: function() {
if (this.get("window_focus")) {
this.set("waiting_messages", 0);
}
},
messages_change: function() {
if (! openerp.webclient || !openerp.webclient.set_title_part)
return;
openerp.webclient.set_title_part("im_messages", this.get("waiting_messages") === 0 ? undefined :
_.str.sprintf(_t("%d Messages"), this.get("waiting_messages")));
},
chat_with_users: function(users) {
var self = this;
return im_common.connection.model("im.session").call("session_get", [_.map(users, function(user) {return user.get("id");}),
self.me.get("uuid")]).then(function(session) {
return self.activate_session(session.id, true);
});
},
chat_with_all_users: function() {
var self = this;
return im_common.connection.model("im.user").call("search", [[["uuid", "=", false]]]).then(function(user_ids) {
return self.ensure_users(_.without(user_ids, self.me.get("id")));
}).then(function(users) {
return self.chat_with_users(users);
});
},
activate_session: function(session_id, focus, message) {
var self = this;
var conv = _.find(this.conversations, function(conv) {return conv.session_id == session_id;});
var def = $.when();
if (! conv) {
conv = new im_common.Conversation(this, this, session_id, this.options);
def = conv.appendTo($("body")).then(_.bind(function() {
conv.on("destroyed", this, function() {
this.conversations = _.without(this.conversations, conv);
this.calc_positions();
});
this.conversations.push(conv);
this.calc_positions();
this.trigger("new_conversation", conv);
}, this));
def = def.then(function(){
return self.load_history(conv, message);
});
}
if (focus) {
def = def.then(function() {
conv.focus();
});
}
return def.then(function() {return conv});
},
load_history: function(conv, message){
var self = this;
var domain = [["session_id", "=", conv.session_id]];
if (!_.isUndefined(message)){
domain.push(["date", "<", message.date]);
}
return im_common.connection.model("im.message").call("search_read", [domain, [], 0, 10]).then(function(messages){
messages.reverse();
var users = _.unique(_.map(messages, function(message){
return message.from_id[0];
}));
return self.ensure_users(users).then(function(){
return self.received_messages(messages, true);
});
});
},
received_messages: function(messages, seen) {
var self = this;
var defs = [];
var received = false;
if (_.isUndefined(seen)){
seen = false;
}
_.each(messages, function(message) {
if (! message.technical) {
defs.push(self.activate_session(message.session_id[0], false, message).then(function(conv) {
received = self.my_id !== message.from_id[0];
return conv.received_message(message);
}));
} else {
var json = JSON.parse(message.message);
message.json = json;
defs.push($.when(im_common.technical_messages_handlers[json.type](self, message)));
}
});
return $.when.apply($, defs).then(function(){
if (! self.get("window_focus") && received && !seen) {
self.set("waiting_messages", self.get("waiting_messages") + messages.length);
self.ting.play();
self.create_ting();
}
});
},
calc_positions: function() {
var current = this.get("right_offset");
_.each(_.range(this.conversations.length), function(i) {
this.conversations[i].set("bottom_position", this.get("bottom_offset"));
this.conversations[i].set("right_position", current);
current += this.conversations[i].$().outerWidth(true);
}, this);
},
destroy: function() {
$(window).off("unload", this.unload_event_handler);
$(window).unbind("blur", this.blur_hdl);
$(window).unbind("focus", this.focus_hdl);
openerp.PropertiesMixin.destroy.call(this);
}
});
im_common.Conversation = openerp.Widget.extend({
className: "openerp_style oe_im_chatview",
events: {
"keydown input": "keydown",
"click .oe_im_chatview_close": "close",
"click .oe_im_chatview_header": "show_hide"
},
init: function(parent, c_manager, session_id, options) {
this._super(parent);
this.c_manager = c_manager;
this.options = options || {};
this.session_id = session_id;
this.set("right_position", 0);
this.set("bottom_position", 0);
this.shown = true;
this.set("pending", 0);
this.inputPlaceholder = this.options.defaultInputPlaceholder;
this.set("users", []);
this.set("disconnected", false);
this.others = [];
},
start: function() {
var self = this;
self.$().append(openerp.qweb.render("im_common.conversation", {widget: self}));
this.$().hide();
var change_status = function() {
var disconnected = _.every(this.get("users"), function(u) { return u.get("im_status") === false; });
self.set("disconnected", disconnected);
this.$(".oe_im_chatview_users").html(openerp.qweb.render("im_common.conversation.header",
{widget: self, to_url: _.bind(im_common.connection.url, im_common.connection)}));
};
this.on("change:users", this, function(unused, ev) {
_.each(ev.oldValue, function(user) {
user.off("change:im_status", self, change_status);
});
_.each(ev.newValue, function(user) {
user.on("change:im_status", self, change_status);
});
change_status.call(self);
_.each(ev.oldValue, function(user) {
if (! _.contains(ev.newValue, user)) {
user.remove_watcher();
}
});
_.each(ev.newValue, function(user) {
if (! _.contains(ev.oldValue, user)) {
user.add_watcher();
}
});
});
this.on("change:disconnected", this, function() {
self.$().toggleClass("oe_im_chatview_disconnected_status", this.get("disconnected"));
self._go_bottom();
});
self.on("change:right_position", self, self.calc_pos);
self.on("change:bottom_position", self, self.calc_pos);
self.full_height = self.$().height();
self.calc_pos();
self.on("change:pending", self, _.bind(function() {
if (self.get("pending") === 0) {
self.$(".oe_im_chatview_nbr_messages").text("");
} else {
self.$(".oe_im_chatview_nbr_messages").text("(" + self.get("pending") + ")");
}
}, self));
return this.refresh_users().then(function() {
self.$().show();
});
},
refresh_users: function() {
var self = this;
var user_ids;
return im_common.connection.model("im.session").call("get_session_users", [self.session_id]).then(function(session) {
user_ids = _.without(session.user_ids, self.c_manager.me.get("id"));
return self.c_manager.ensure_users(user_ids);
}).then(function(users) {
self.set("users", users);
});
},
show_hide: function() {
if (this.shown) {
this.$().animate({
height: this.$(".oe_im_chatview_header").outerHeight()
});
} else {
this.$().animate({
height: this.full_height
});
}
this.shown = ! this.shown;
if (this.shown) {
this.set("pending", 0);
}
},
calc_pos: function() {
this.$().css("right", this.get("right_position"));
this.$().css("bottom", this.get("bottom_position"));
},
received_message: function(message) {
if (this.shown) {
this.set("pending", 0);
} else {
this.set("pending", this.get("pending") + 1);
}
this.c_manager.ensure_users([message.from_id[0]]).then(_.bind(function(users) {
var user = users[0];
if (! _.contains(this.get("users"), user) && ! _.contains(this.others, user)) {
this.others.push(user);
user.add_watcher();
}
this._add_bubble(user, message.message, openerp.str_to_datetime(message.date));
}, this));
},
keydown: function(e) {
if(e && e.which !== 13) {
return;
}
var mes = this.$("input").val();
if (! mes.trim()) {
return;
}
this.$("input").val("");
this.send_message(mes);
},
send_message: function(message, technical) {
technical = technical || false;
var send_it = _.bind(function() {
var model = im_common.connection.model("im.message");
return model.call("post", [message, this.session_id, technical], {uuid: this.c_manager.me.get("uuid"), context: {}});
}, this);
var tries = 0;
send_it().then(_.bind(function() {}, function(error, e) {
e.preventDefault();
tries += 1;
if (tries < 3)
return send_it();
}));
},
_add_bubble: function(user, item, date) {
var items = [item];
if (user === this.last_user) {
this.last_bubble.remove();
items = this.last_items.concat(items);
}
this.last_user = user;
this.last_items = items;
var zpad = function(str, size) {
str = "" + str;
return new Array(size - str.length + 1).join('0') + str;
};
date = "" + zpad(date.getHours(), 2) + ":" + zpad(date.getMinutes(), 2);
var to_show = _.map(items, im_common.escape_keep_url);
this.last_bubble = $(openerp.qweb.render("im_common.conversation_bubble", {"items": to_show, "user": user, "time": date}));
$(this.$(".oe_im_chatview_conversation")).append(this.last_bubble);
this._go_bottom();
},
_go_bottom: function() {
this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height());
},
add_user: function(user) {
if (user === this.me || _.contains(this.get("users"), user))
return;
im_common.connection.model("im.session").call("add_to_session",
[this.session_id, user.get("id"), this.c_manager.me.get("uuid")]).then(_.bind(function() {
this.send_message(JSON.stringify({"type": "session_modified", "action": "added", "user_id": user.get("id")}), true);
}, this));
},
focus: function() {
this.$(".oe_im_chatview_input").focus();
if (! this.shown)
this.show_hide();
},
close: function() {
var def = $.when();
if (this.get("users").length > 1) {
def = im_common.connection.model("im.session").call("remove_me_from_session",
[this.session_id, this.c_manager.me.get("uuid")]).then(_.bind(function() {
return this.send_message(JSON.stringify({"type": "session_modified", "action": "removed",
"user_id": this.c_manager.me.get("id")}), true)
}, this))
}
return def.then(_.bind(function() {
this.destroy();
}, this));
},
destroy: function() {
_.each(this.get("users"), function(user) {
user.remove_watcher();
})
_.each(this.others, function(user) {
user.remove_watcher();
})
this.trigger("destroyed");
return this._super();
}
});
im_common.technical_messages_handlers = {};
im_common.technical_messages_handlers.session_modified = function(c_manager, message) {
var def = $.when();
if (message.json.action === "added" && message.json.user_id === c_manager.me.get("id")) {
def = c_manager.activate_session(message.session_id[0], true);
}
return def.then(function() {
var conv = _.find(c_manager.conversations, function(conv) {return conv.session_id == message.session_id[0];});
if (conv)
return conv.refresh_users();
return undefined;
});
};
var url_regex = /(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/gi;
im_common.escape_keep_url = function(str) {
var last = 0;
var txt = "";
while (true) {
var result = url_regex.exec(str);
if (! result)
break;
txt += _.escape(str.slice(last, result.index));
last = url_regex.lastIndex;
var url = _.escape(result[0]);
txt += '<a href="' + url + '" target="_blank">' + url + '</a>';
}
txt += _.escape(str.slice(last, str.length));
return txt;
};
return im_common;
}
if (typeof(define) !== "undefined") {
define(["jquery", "underscore", "openerp"], declare);
} else {
window.im_common = declare($, _, openerp);
}
})();

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1:
-->
<templates xml:space="preserve">
<t t-name="InstantMessaging">
<div class="oe_im">
<div class="oe_im_frame_header">
<span class="oe_e oe_im_search_icon">ô</span>
<input class="oe_im_searchbox" t-att-placeholder="_t('Search users...')"/>
<span class="oe_e oe_im_search_clear">[</span>
</div>
<div class="oe_im_users"></div>
<div class="oe_im_content"></div>
</div>
</t>
<t t-name="ImTopButton">
<li t-att-title='_t("Display Instant Messaging")'>
<a href="#">
<i id="oe_topbar_imbutton_icon" class="fa fa-comments-o"/>
</a>
</li>
</t>
<t t-name="UserWidget">
<div class="oe_im_user">
<span class="oe_im_user_clip">
<img t-att-src='widget.user.get("image_url")' class="oe_im_user_avatar"/>
</span>
<span class="oe_im_user_name"><t t-esc="widget.user.get('name')"/></span>
<img t-att-src="_s +'/im/static/src/img/green.png'" class="oe_im_user_online"/>
</div>
</t>
</templates>

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="im_common.conversation">
<div class="oe_im_chatview_header">
<span class="oe_im_chatview_users"/>
<scan class="oe_im_chatview_nbr_messages" />
<button class="oe_im_chatview_close">×</button>
</div>
<div class="oe_im_chatview_disconnected">
All users are offline. They will receive your messages on their next connection.
</div>
<div class="oe_im_chatview_content">
<div class="oe_im_chatview_conversation"></div>
</div>
<div class="oe_im_chatview_footer">
<input class="oe_im_chatview_input" t-att-placeholder="widget.inputPlaceholder" />
</div>
</t>
<t t-name="im_common.conversation.header">
<span t-foreach="widget.get('users')" t-as="user">
<img t-if="user.get('im_status')" t-att-src="to_url('/im/static/src/img/green.png')" class="oe_im_chatview_online"/>
<t t-esc="user.get('name')"/>
</span>
</t>
<t t-name="im_common.conversation_bubble">
<div class="oe_im_chatview_bubble">
<div class="oe_im_chatview_clip">
<img class="oe_im_chatview_avatar" t-att-src="user.get('image_url')"/>
</div>
<div class="oe_im_chatview_from"><t t-esc="user.get('name')"/></div>
<div class="oe_im_chatview_bubble_list">
<t t-foreach="items" t-as="item">
<div class="oe_im_chatview_bubble_item"><t t-raw="item"/></div>
</t>
</div>
<div class="oe_im_chatview_time"><t t-esc="time"/></div>
</div>
</t>
</templates>

View File

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- vim:fdn=3:
-->
<openerp>
<data>
<template id="assets_backend" name="im assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/im/static/src/css/im.css"/>
<link rel="stylesheet" href="/im/static/src/css/im_common.css"/>
<script type="text/javascript" src="/im/static/src/js/im_common.js"></script>
<script type="text/javascript" src="/im/static/src/js/im.js"></script>
</xpath>
</template>
</data>
</openerp>

View File

@ -1,85 +0,0 @@
import openerp
import openerp.tools.config
import openerp.modules.registry
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
import datetime
from openerp.osv import osv, fields
import time
import logging
import json
import select
import gevent
import gevent.event
from openerp.addons.im.im import *
_logger = logging.getLogger(__name__)
class ImWatcher(object):
watchers = {}
@staticmethod
def get_watcher(db_name):
if not ImWatcher.watchers.get(db_name):
ImWatcher(db_name)
return ImWatcher.watchers[db_name]
def __init__(self, db_name):
self.db_name = db_name
ImWatcher.watchers[db_name] = self
self.waiting = 0
self.wait_id = 0
self.users = {}
self.users_watch = {}
gevent.spawn(self.loop)
def loop(self):
_logger.info("Begin watching on channel im_channel for database " + self.db_name)
stop = False
while not stop:
try:
registry = openerp.modules.registry.RegistryManager.get(self.db_name)
with registry.cursor() as cr:
listen_channel(cr, "im_channel", self.handle_message, self.check_stop)
stop = True
except:
# if something crash, we wait some time then try again
_logger.exception("Exception during watcher activity")
time.sleep(WATCHER_ERROR_DELAY)
_logger.info("End watching on channel im_channel for database " + self.db_name)
del ImWatcher.watchers[self.db_name]
def handle_message(self, message):
if message["type"] == "message":
for receiver in message["receivers"]:
for waiter in self.users.get(receiver, {}).values():
waiter.set()
else: #type status
for waiter in self.users_watch.get(message["user"], {}).values():
waiter.set()
def check_stop(self):
return self.waiting == 0
def _get_wait_id(self):
self.wait_id += 1
return self.wait_id
def stop(self, user_id, watch_users, timeout=None):
wait_id = self._get_wait_id()
event = gevent.event.Event()
self.waiting += 1
self.users.setdefault(user_id, {})[wait_id] = event
for watch in watch_users:
self.users_watch.setdefault(watch, {})[wait_id] = event
try:
event.wait(timeout)
finally:
for watch in watch_users:
del self.users_watch[watch][wait_id]
if len(self.users_watch[watch]) == 0:
del self.users_watch[watch]
del self.users[user_id][wait_id]
if len(self.users[user_id]) == 0:
del self.users[user_id]
self.waiting -= 1

View File

@ -0,0 +1 @@
import im_chat

View File

@ -1,7 +1,8 @@
{
'name' : 'Instant Messaging',
'version': '1.0',
'summary': 'Live Chat, Talks with Others',
'summary': 'OpenERP Chat',
'author': 'OpenERP SA',
'sequence': '18',
'category': 'Tools',
'complexity': 'easy',
@ -16,11 +17,9 @@ chat in real time. It support several chats in parallel.
'data': [
'security/ir.model.access.csv',
'security/im_security.xml',
'views/im.xml',
'views/im_chat.xml',
],
'depends' : ['base', 'web'],
'depends' : ['base', 'web', 'bus'],
'qweb': ['static/src/xml/*.xml'],
'installable': True,
'auto_install': False,
'application': True,
}

364
addons/im_chat/im_chat.py Normal file
View File

@ -0,0 +1,364 @@
# -*- coding: utf-8 -*-
import base64
import datetime
import logging
import time
import uuid
import random
import simplejson
import openerp
from openerp.http import request
from openerp.osv import osv, fields
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.addons.bus.bus import TIMEOUT
_logger = logging.getLogger(__name__)
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. """
_name = 'im_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'
}
class im_chat_session(osv.Model):
""" Conversations."""
_order = 'id desc'
_name = 'im_chat.session'
_rec_name = 'uuid'
_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 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
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
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
def session_get(self, cr, uid, user_to, context=None):
""" returns the canonical session between 2 users, create it if needed """
session_id = False
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)
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):
if not state:
state = sr.state
if sr.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())
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)
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]
image_b64 = res["image_small"]
return image_b64
class im_chat_message(osv.Model):
""" Sessions messsages type can be 'message' or 'meta'.
For anonymous message, the from_id is False.
Messages are sent to a session not to users.
"""
_name = 'im_chat.message'
_order = "id desc"
_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',
}
def init_messages(self, cr, uid, context=None):
""" get unread messages and old messages received less than AWAY_TIMER
ago and the session_info for open or folded window
"""
# 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)]
# 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)
# 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)
reopening_session = []
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)
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)
notifications = []
for session in Session.browse(cr, uid, session_ids, context=context):
# build the new message
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
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
"""
_name = 'im_chat.presence'
_columns = {
'user_id' : fields.many2one('res.users', 'Users', required=True, select=True),
'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.')]
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)
# set the default values
send_notification = True
vals = {
'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'status' : presences and presences[0].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'
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']
# 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):
self.write(cr, uid, presence_ids, vals, context=context)
# avoid TransactionRollbackError
cr.commit()
# 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']})
# 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)
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)
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
class res_users(osv.Model):
_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
_columns = {
'im_status' : fields.function(_get_im_status, type="char", string="IM Status"),
}
def im_search(self, cr, uid, name, limit, context=None):
""" search users with a name and return its id, name and im_status """
group_user_id = self.pool.get("ir.model.data").get_object_reference(cr, uid, 'base', 'group_user')[1]
user_ids = self.name_search(cr, uid, name, [('id','!=', uid), ('groups_id', 'in', [group_user_id])], limit=limit, context=context)
domain = [('user_id', 'in', [i[0] for i in user_ids])]
ids = self.pool['im_chat.presence'].search(cr, uid, domain, order="last_poll desc", context=context)
presences = self.pool['im_chat.presence'].read(cr, uid, ids, ['user_id','status'], context=context)
res = []
for user_id in user_ids:
user = {
'id' : user_id[0],
'name' : user_id[1]
}
tmp = filter(lambda p: p['user_id'][0] == user_id[0], presences)
user['im_status'] = len(tmp) > 0 and tmp[0]['status'] or 'offline'
res.append(user)
return res
#----------------------------------------------------------
# Controllers
#----------------------------------------------------------
class Controller(openerp.addons.bus.bus.Controller):
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, ('im_presence' in options), context=context)
# listen to connection and disconnections
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
@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
@openerp.http.route(['/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
image_data = base64.b64decode(image_b64)
headers = [('Content-Type', 'image/png')]
headers.append(('Content-Length', len(image_data)))
return request.make_response(image_data, headers)
# vim:et:

View File

@ -0,0 +1,37 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="message_rule_1" 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="[(6,0,[ref('base.group_user')])]"/>
<field name="domain_force">[('to_id.user_ids', 'in', [user.id])]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="users_rule_1" 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="[(6,0,[ref('base.group_user')])]"/>
<field name="domain_force">[('user_ids', 'in', [user.id])]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="session_relation_rule_1" 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="[(6,0,[ref('base.group_user')])]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
</data>
</openerp>

View File

@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_im_chat_message,im_chat.message,model_im_chat_message,base.group_user,1,0,1,0
access_im_chat_session,im_chat.session,model_im_chat_session,base.group_user,1,1,1,0
access_im_chat_conversation_state,im_chat.conversation_state,model_im_chat_conversation_state,base.group_user,1,1,1,0
access_im_chat_presence,im_chat.presence,model_im_chat_presence,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_im_chat_message im_chat.message model_im_chat_message base.group_user 1 0 1 0
3 access_im_chat_session im_chat.session model_im_chat_session base.group_user 1 1 1 0
4 access_im_chat_conversation_state im_chat.conversation_state model_im_chat_conversation_state base.group_user 1 1 1 0
5 access_im_chat_presence im_chat.presence model_im_chat_presence base.group_user 1 1 1 1

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -40,7 +40,7 @@
.oe_im_frame_header .oe_im_search_icon {
position: absolute;
color: #888;
top: 2px;
top: -4px;
left: 9px;
font-size: 28px;
font-family: "entypoRegular" !important;
@ -106,4 +106,4 @@
height: 11px;
vertical-align: middle;
border: 0;
}
}

View File

@ -73,19 +73,36 @@
border-bottom: 1px solid #AEB9BD;
cursor: pointer;
}
.oe_im_chatview .oe_im_chatview_close {
.oe_im_chatview .oe_im_chatview_header_name{
max-width: 75%;
word-wrap: break-word;
display: inline-block;
height: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.oe_im_chatview .oe_im_chatview_right {
padding: 0;
background: transparent;
border: 0;
float: right;
color: gray ;
}
.oe_im_chatview .oe_im_chatview_right div, .oe_im_chatview .oe_im_chatview_right button{
cursor: pointer;
background: transparent;
border: 0;
margin-left: 5px;
-webkit-appearance: none;
font-size: 18px;
line-height: 16px;
float: right;
font-weight: bold;
color: black;
text-shadow: 0 1px 0 white;
opacity: 0.2;
opacity: 0.9;
display: inline-block;
}
.oe_im_chatview .oe_im_chatview_content {
overflow: auto;
@ -120,15 +137,34 @@
-webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
}
.oe_im_chatview .oe_im_chatview_bubble {
.oe_im_chatview .oe_im_chatview_message_bubble {
background: white;
position: relative;
min-height: 32px;
padding: 3px;
margin: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
}
.oe_im_chatview .oe_im_chatview_message_bubble .smiley{
font-size: 18px;
}
.oe_im_chatview .oe_im_chatview_technical_bubble {
background: #D8D8D8;
position: relative;
min-height: 32px;
padding: 3px;
margin: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
font-style:italic;
}
.oe_im_chatview .oe_im_chatview_clip {
position: relative;
float: left;
@ -187,3 +223,13 @@
width: 11px;
height: 11px;
}
.oe_im_chatview .oe_im_chatview_date_separator {
background: transparent;
position: relative;
padding: 3px;
color: #aaa;
margin: 3px;
text-align: right;
font-size: 12px;
}

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 74 B

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 830 B

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

View File

@ -0,0 +1,596 @@
(function(){
"use strict";
var _t = openerp._t;
var _lt = openerp._lt;
var QWeb = openerp.qweb;
var NBR_LIMIT_HISTORY = 20;
var USERS_LIMIT = 20;
var im_chat = openerp.im_chat = {};
im_chat.ConversationManager = openerp.Widget.extend({
init: function(parent, options) {
var self = this;
this._super(parent);
this.options = _.clone(options) || {};
_.defaults(this.options, {
inputPlaceholder: _t("Say something..."),
defaultMessage: null,
defaultUsername: _t("Visitor"),
});
// business
this.sessions = {};
this.bus = openerp.bus.bus;
this.bus.on("notification", this, this.on_notification);
this.bus.options["im_presence"] = true;
// ui
this.set("right_offset", 0);
this.set("bottom_offset", 0);
this.on("change:right_offset", this, this.calc_positions);
this.on("change:bottom_offset", this, this.calc_positions);
this.set("window_focus", true);
this.on("change:window_focus", self, function(e) {
self.bus.options["im_presence"] = self.get("window_focus");
});
this.set("waiting_messages", 0);
this.on("change:waiting_messages", this, this.window_title_change);
$(window).on("focus", _.bind(this.window_focus, this));
$(window).on("blur", _.bind(this.window_blur, this));
this.window_title_change();
},
on_notification: function(notification) {
var self = this;
var channel = notification[0];
var message = notification[1];
var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g);
// Concern im_chat : if the channel is the im_chat.session or im_chat.status, or a 'private' channel (aka the UUID of a session)
if((Array.isArray(channel) && (channel[1] === 'im_chat.session' || channel[1] === 'im_chat.presence')) || (regex_uuid.test(channel))){
// message to display in the chatview
if (message.type === "message" || message.type === "meta") {
self.received_message(message);
}
// activate the received session
if(message.uuid){
this.apply_session(message);
}
// user status notification
if(message.im_status){
self.trigger("im_new_user_status", [message]);
}
}
},
// window focus unfocus beep and title
window_focus: function() {
this.set("window_focus", true);
this.set("waiting_messages", 0);
},
window_blur: function() {
this.set("window_focus", false);
},
window_beep: function() {
if (typeof(Audio) === "undefined") {
return;
}
var audio = new Audio();
var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
var kitten = jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
audio.src = openerp.session.url("/im_chat/static/src/audio/" + (kitten ? "purr" : "ting") + ext);
audio.play();
},
window_title_change: function() {
var title = undefined;
if (this.get("waiting_messages") !== 0) {
title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages"))
this.window_beep();
}
if (! openerp.webclient || !openerp.webclient.set_title_part)
return;
openerp.webclient.set_title_part("im_messages", title);
},
apply_session: function(session, focus){
var self = this;
var conv = this.sessions[session.uuid];
if (! conv) {
if(session.state !== 'closed'){
conv = new im_chat.Conversation(this, this, session, this.options);
conv.appendTo($("body"));
conv.on("destroyed", this, function() {
delete this.sessions[session.uuid];
this.calc_positions();
});
this.sessions[session.uuid] = conv;
this.calc_positions();
}
}else{
conv.set("session", session);
}
conv && this.trigger("im_session_activated", conv);
if (focus)
conv.focus();
return conv;
},
activate_session: function(session, focus) {
var self = this;
var active_session = _.clone(session);
active_session.state = 'open';
var conv = this.apply_session(active_session, focus);
if(session.state !== 'open'){
conv.update_fold_state('open');
}
return conv;
},
received_message: function(message) {
var self = this;
var session_id = message.to_id[0];
var uuid = message.to_id[1];
if (! this.get("window_focus")) {
this.set("waiting_messages", this.get("waiting_messages") + 1);
}
var conv = this.sessions[uuid];
if(!conv){
// fetch the session, and init it with the message
var def_session = new openerp.Model("im_chat.session").call("session_info", [], {"ids" : [session_id]}).then(function(session){
conv = self.activate_session(session, false);
conv.received_message(message);
});
}else{
conv.received_message(message);
}
},
calc_positions: function() {
var self = this;
var current = this.get("right_offset");
_.each(this.sessions, function(s) {
s.set("bottom_position", self.get("bottom_offset"));
s.set("right_position", current);
current += s.$().outerWidth(true);
});
},
destroy: function() {
$(window).off("unload", this.unload);
$(window).off("focus", this.window_focus);
$(window).off("blur", this.window_blur);
return this._super();
}
});
im_chat.Conversation = openerp.Widget.extend({
className: "openerp_style oe_im_chatview",
events: {
"keydown input": "keydown",
"click .oe_im_chatview_close": "click_close",
"click .oe_im_chatview_header": "click_header"
},
init: function(parent, c_manager, session, options) {
this._super(parent);
this.c_manager = c_manager;
this.options = options || {};
this.loading_history = true;
this.set("messages", []);
this.set("session", session);
this.set("right_position", 0);
this.set("bottom_position", 0);
this.set("pending", 0);
this.inputPlaceholder = this.options.defaultInputPlaceholder;
},
start: function() {
var self = this;
self.$().append(openerp.qweb.render("im_chat.Conversation", {widget: self}));
self.$().hide();
self.on("change:session", self, self.update_session);
self.on("change:right_position", self, self.calc_pos);
self.on("change:bottom_position", self, self.calc_pos);
self.full_height = self.$().height();
self.calc_pos();
self.on("change:pending", self, _.bind(function() {
if (self.get("pending") === 0) {
self.$(".oe_im_chatview_nbr_messages").text("");
} else {
self.$(".oe_im_chatview_nbr_messages").text("(" + self.get("pending") + ")");
}
}, self));
// messages business
self.on("change:messages", this, this.render_messages);
self.$('.oe_im_chatview_content').on('scroll',function(){
if($(this).scrollTop() === 0){
self.load_history();
}
});
self.load_history();
self.$().show();
// prepare the header and the correct state
self.update_session();
},
show: function(){
this.$().animate({
height: this.full_height
});
this.set("pending", 0);
},
hide: function(){
this.$().animate({
height: this.$(".oe_im_chatview_header").outerHeight()
});
},
calc_pos: function() {
this.$().css("right", this.get("right_position"));
this.$().css("bottom", this.get("bottom_position"));
},
update_fold_state: function(state){
return new openerp.Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state});
},
update_session: function(){
// built the name
var names = [];
_.each(this.get("session").users, function(user){
if( (openerp.session.uid !== user.id) && !(_.isUndefined(openerp.session.uid) && !user.id) ){
names.push(user.name);
}
});
this.$(".oe_im_chatview_header_name").text(names.join(", "));
this.$(".oe_im_chatview_header_name").attr('title', names.join(", "));
// update the fold state
if(this.get("session").state){
if(this.get("session").state === 'closed'){
this.destroy();
}else{
if(this.get("session").state === 'open'){
this.show();
}else{
this.hide();
}
}
}
},
load_history: function(){
var self = this;
if(this.loading_history){
var domain = [["to_id.uuid", "=", this.get("session").uuid]];
_.first(this.get("messages")) && domain.push(['id','<', _.first(this.get("messages")).id]);
new openerp.Model("im_chat.message").call("search_read", [domain, ['id', 'create_date','to_id','from_id', 'type', 'message'], 0, NBR_LIMIT_HISTORY]).then(function(messages){
self.insert_messages(messages);
if(messages.length != NBR_LIMIT_HISTORY){
self.loading_history = false;
}
});
}
},
received_message: function(message) {
if (this.get('session').state === 'open') {
this.set("pending", 0);
} else {
this.set("pending", this.get("pending") + 1);
}
this.insert_messages([message]);
this._go_bottom();
},
send_message: function(message, type) {
var self = this;
var send_it = function() {
return openerp.session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message});
};
var tries = 0;
send_it().fail(function(error, e) {
e.preventDefault();
tries += 1;
if (tries < 3)
return send_it();
});
},
insert_messages: function(messages){
var self = this;
// avoid duplicated messages
messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; });
// escape the message content and set the timezone
_.map(messages, function(m){
if(!m.from_id){
m.from_id = [false, self.options["defaultUsername"]];
}
m.message = self.escape_keep_url(m.message);
m.message = self.smiley(m.message);
m.create_date = Date.parse(m.create_date).setTimezone("UTC").toString("yyyy-dd-MM HH:mm:ss");
return m;
});
this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; }));
},
render_messages: function(){
var self = this;
var res = {};
var last_date_day, last_user_id = -1;
_.each(this.get("messages"), function(current){
// add the url of the avatar for all users in the conversation
current.from_id[2] = openerp.session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0]));
var date_day = current.create_date.split(" ")[0];
if(date_day !== last_date_day){
res[date_day] = [];
last_user_id = -1;
}
last_date_day = date_day;
if(current.type == "message"){ // traditionnal message
if(last_user_id === current.from_id[0]){
_.last(res[date_day]).push(current);
}else{
res[date_day].push([current]);
}
last_user_id = current.from_id[0];
}else{ // meta message
res[date_day].push([current]);
last_user_id = -1;
}
});
// render and set the content of the chatview
this.$('.oe_im_chatview_content_bubbles').html($(openerp.qweb.render("im_chat.Conversation_content", {"list": res})));
},
keydown: function(e) {
if(e && e.which !== 13) {
return;
}
var mes = this.$("input").val();
if (! mes.trim()) {
return;
}
this.$("input").val("");
this.send_message(mes, "message");
},
get_smiley_list: function(){
var kitten = jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
var smileys = {
":'(": "&#128546;",
":O" : "&#128561;",
"3:)": "&#128520;",
":)" : "&#128522;",
":D" : "&#128517;",
";)" : "&#128521;",
":p" : "&#128523;",
":(" : "&#9785;",
":|" : "&#128528;",
":/" : "&#128527;",
"8)" : "&#128563;",
":s" : "&#128534;",
":pinky" : "<img src='/im_chat/static/src/img/pinky.png'/>",
":musti" : "<img src='/im_chat/static/src/img/musti.png'/>",
};
if(kitten){
_.extend(smileys, {
":)" : "&#128570;",
":D" : "&#128569;",
";)" : "&#128572;",
":p" : "&#128573;",
":(" : "&#128576;",
":|" : "&#128575;",
});
}
return smileys;
},
smiley: function(str){
var re_escape = function(str){
return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
};
var smileys = this.get_smiley_list();
_.each(_.keys(smileys), function(key){
str = str.replace( new RegExp("(?:^|\\s)(" + re_escape(key) + ")(?:\\s|$)"), ' <span class="smiley">'+smileys[key]+'</span> ');
});
return str;
},
escape_keep_url: function(str){
var url_regex = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi;
var last = 0;
var txt = "";
while (true) {
var result = url_regex.exec(str);
if (! result)
break;
txt += _.escape(str.slice(last, result.index));
last = url_regex.lastIndex;
var url = _.escape(result[0]);
txt += '<a href="' + url + '" target="_blank">' + url + '</a>';
}
txt += _.escape(str.slice(last, str.length));
return txt;
},
_go_bottom: function() {
this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight);
},
add_user: function(user){
return new openerp.Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]);
},
focus: function() {
this.$(".oe_im_chatview_input").focus();
},
click_header: function(){
this.update_fold_state();
},
click_close: function(event) {
event.stopPropagation();
this.update_fold_state('closed');
},
destroy: function() {
this.trigger("destroyed");
return this._super();
}
});
im_chat.UserWidget = openerp.Widget.extend({
"template": "im_chat.UserWidget",
events: {
"click": "activate_user",
},
init: function(parent, user) {
this._super(parent);
this.set("id", user.id);
this.set("name", user.name);
this.set("im_status", user.im_status);
this.set("image_url", user.image_url);
},
start: function() {
this.$el.data("user", {id:this.get("id"), name:this.get("name")});
this.$el.draggable({helper: "clone"});
this.on("change:im_status", this, this.update_status);
this.update_status();
},
update_status: function(){
this.$(".oe_im_user_online").toggle(this.get('im_status') !== 'offline');
var img_src = (this.get('im_status') == 'away' ? '/im_chat/static/src/img/yellow.png' : '/im_chat/static/src/img/green.png');
this.$(".oe_im_user_online").attr('src', openerp.session.server + img_src);
},
activate_user: function() {
this.trigger("activate_user", this.get("id"));
},
});
im_chat.InstantMessaging = openerp.Widget.extend({
template: "im_chat.InstantMessaging",
events: {
"keydown .oe_im_searchbox": "input_change",
"keyup .oe_im_searchbox": "input_change",
"change .oe_im_searchbox": "input_change",
},
init: function(parent) {
this._super(parent);
this.shown = false;
this.set("right_offset", 0);
this.set("current_search", "");
this.users = [];
this.widgets = {};
this.c_manager = new openerp.im_chat.ConversationManager(this);
this.on("change:right_offset", this.c_manager, _.bind(function() {
this.c_manager.set("right_offset", this.get("right_offset"));
}, this));
this.user_search_dm = new openerp.web.DropMisordered();
},
start: function() {
var self = this;
this.$el.css("right", -this.$el.outerWidth());
$(window).scroll(_.bind(this.calc_box, this));
$(window).resize(_.bind(this.calc_box, this));
this.calc_box();
this.on("change:current_search", this, this.search_changed);
this.search_changed();
// add a drag & drop listener
self.c_manager.on("im_session_activated", self, function(conv) {
conv.$el.droppable({
drop: function(event, ui) {
conv.add_user(ui.draggable.data("user"));
}
});
});
// add a listener for the update of users status
this.c_manager.on("im_new_user_status", this, this.update_users_status);
// fetch the unread message and the recent activity (e.i. to re-init in case of refreshing page)
openerp.session.rpc("/im_chat/init",{}).then(function(notifications) {
_.each(notifications, function(notif){
self.c_manager.on_notification(notif);
});
// start polling
openerp.bus.bus.start_polling();
});
return;
},
calc_box: function() {
var $topbar = window.$('#oe_main_menu_navbar'); // .oe_topbar is replaced with .navbar of bootstrap3
var top = $topbar.offset().top + $topbar.height();
top = Math.max(top - $(window).scrollTop(), 0);
this.$el.css("top", top);
this.$el.css("bottom", 0);
},
input_change: function() {
this.set("current_search", this.$(".oe_im_searchbox").val());
},
search_changed: function(e) {
var user_model = new openerp.web.Model("res.users");
var self = this;
return this.user_search_dm.add(user_model.call("im_search", [this.get("current_search"),
USERS_LIMIT], {context:new openerp.web.CompoundContext()})).then(function(result) {
self.$(".oe_im_input").val("");
var old_widgets = self.widgets;
self.widgets = {};
self.users = [];
_.each(result, function(user) {
user.image_url = openerp.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: user.id});
var widget = new openerp.im_chat.UserWidget(self, user);
widget.appendTo(self.$(".oe_im_users"));
widget.on("activate_user", self, self.activate_user);
self.widgets[user.id] = widget;
self.users.push(user);
});
_.each(old_widgets, function(w) {
w.destroy();
});
});
},
switch_display: function() {
var fct = _.bind(function(place) {
this.set("right_offset", place + this.$el.outerWidth());
}, this);
var opt = {
step: fct,
};
if (this.shown) {
this.$el.animate({
right: -this.$el.outerWidth(),
}, opt);
} else {
if (! openerp.bus.bus.activated) {
this.do_warn("Instant Messaging is not activated on this server. Try later.", "");
return;
}
this.$el.animate({
right: 0,
}, opt);
}
this.shown = ! this.shown;
},
activate_user: function(user_id) {
var self = this;
var sessions = new openerp.web.Model("im_chat.session");
return sessions.call("session_get", [user_id]).then(function(session) {
self.c_manager.activate_session(session, true);
});
},
update_users_status: function(users_list){
var self = this;
_.each(users_list, function(el) {
self.widgets[el.id] && self.widgets[el.id].set("im_status", el.im_status);
});
}
});
im_chat.ImTopButton = openerp.Widget.extend({
template:'im_chat.ImTopButton',
events: {
"click": "clicked",
},
clicked: function(ev) {
ev.preventDefault();
this.trigger("clicked");
},
});
if(openerp.web && openerp.web.UserMenu) {
openerp.web.UserMenu.include({
do_update: function(){
var self = this;
this.update_promise.then(function() {
var im = new openerp.im_chat.InstantMessaging(self);
openerp.im_chat.single = im;
im.appendTo(openerp.client.$el);
var button = new openerp.im_chat.ImTopButton(this);
button.on("clicked", im, im.switch_display);
button.appendTo(window.$('.oe_systray'));
});
return this._super.apply(this, arguments);
},
});
}
return im_chat;
})();

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1:
-->
<templates xml:space="preserve">
<t t-name="im_chat.Conversation">
<div class="oe_im_chatview_header">
<span class="oe_im_chatview_header_name"></span>
<span class="oe_im_chatview_nbr_messages"/>
<span class="oe_im_chatview_right">
<div class="oe_im_chatview_close">×</div>
</span>
</div>
<div class="oe_im_chatview_content">
<div class="oe_im_chatview_status"/>
<div class="oe_im_chatview_content_bubbles"></div>
</div>
<div class="oe_im_chatview_footer">
<input class="oe_im_chatview_input" t-att-placeholder="widget.inputPlaceholder" />
</div>
</t>
<t t-name="im_chat.Conversation_content">
<t t-foreach="_.keys(list)" t-as="date">
<div class="oe_im_chatview_date_separator">
<t t-esc="Date.parse(date).toString(Date.CultureInfo.formatPatterns.longDate)"/>
</div>
<t t-foreach="list[date]" t-as="bubble">
<t t-if="bubble[0].type === 'message'">
<t t-call="im_chat.Conversation_message_bubble">
<t t-set="messages" t-value="bubble"/>
</t>
</t>
<t t-if="bubble[0].type === 'meta'">
<t t-call="im_chat.Conversation_technical_bubble">
<t t-set="messages" t-value="bubble"/>
</t>
</t>
</t>
</t>
</t>
<t t-name="im_chat.Conversation_message_bubble">
<div class="oe_im_chatview_message_bubble">
<div class="oe_im_chatview_clip">
<img class="oe_im_chatview_avatar" t-att-src="_.last(messages).from_id[2]"/>
</div>
<div class="oe_im_chatview_from"><t t-esc="_.last(messages).from_id[1]"/></div>
<div class="oe_im_chatview_bubble_list">
<t t-foreach="messages" t-as="m">
<div class="oe_im_chatview_bubble_item"><t t-raw="m.message"/></div>
</t>
</div>
<div class="oe_im_chatview_time"><t t-esc="Date.parse((_.last(messages).create_date)).toString('HH:mm')"/></div>
</div>
</t>
<t t-name="im_chat.Conversation_technical_bubble">
<div class="oe_im_chatview_technical_bubble">
<div class="oe_im_chatview_from"><t t-esc="_.last(messages).from_id[1]"/></div>
<div>
<t t-foreach="messages" t-as="m">
<div><t t-raw="m.message"/></div>
</t>
</div>
<div class="oe_im_chatview_time"><t t-esc="Date.parse((_.last(messages).create_date)).toString('HH:mm')"/></div>
</div>
</t>
<t t-name="im_chat.UserWidget">
<div class="oe_im_user ui-draggable">
<span class="oe_im_user_clip">
<img t-att-src="widget.get('image_url')" class="oe_im_user_avatar"/>
</span>
<span class="oe_im_user_name"><t t-esc="widget.get('name')"/></span>
<img t-att-src="_s +'/im_chat/static/src/img/green.png'" t-att-data-im-user-id="widget.get('id')" class="oe_im_user_online"/>
</div>
</t>
<t t-name="im_chat.InstantMessaging">
<div class="oe_im">
<div class="oe_im_frame_header">
<span class="oe_e oe_im_search_icon">ô</span>
<input class="oe_im_searchbox" t-att-placeholder="_t('Search users...')"/>
<span class="oe_e oe_im_search_clear">[</span>
</div>
<div class="oe_im_users"></div>
<div class="oe_im_content"></div>
</div>
</t>
<t t-name="im_chat.ImTopButton">
<li t-att-title='_t("Display Instant Messaging")'>
<a href="#">
<i id="oe_topbar_imbutton_icon" class="fa fa-comments-o"/>
</a>
</li>
</t>
</templates>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- vim:fdn=3:
-->
<openerp>
<data>
<template id="assets_backend" name="im_chat assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/im_chat/static/src/css/im_common.css"/>
<link rel="stylesheet" href="/im_chat/static/src/css/im_chat.css"/>
<script type="text/javascript" src="/im_chat/static/src/js/im_chat.js"></script>
</xpath>
</template>
</data>
</openerp>

View File

@ -1,2 +1 @@
import im_livechat

View File

@ -1,5 +1,6 @@
{
'name' : 'Live Support',
'author': 'OpenERP SA',
'version': '1.0',
'summary': 'Live Chat with Visitors/Customers',
'category': 'Tools',
@ -17,12 +18,13 @@ chat operators.
'data': [
"security/im_livechat_security.xml",
"security/ir.model.access.csv",
"im_livechat_view.xml",
"views/im_livechat_view.xml",
"views/im_livechat.xml"
],
'demo': [
"im_livechat_demo.xml",
],
'depends' : ["im", "mail"],
'depends' : ["mail", "im_chat"],
'installable': True,
'auto_install': False,
'application': True,

View File

@ -19,66 +19,16 @@
#
##############################################################################
import json
import random
import jinja2
import openerp
import openerp.addons.im.im as im
import openerp.addons.im_chat.im_chat
from openerp.osv import osv, fields
from openerp import tools
from openerp import http
from openerp.http import request
env = jinja2.Environment(
loader=jinja2.PackageLoader('openerp.addons.im_livechat', "."),
autoescape=False
)
env.filters["json"] = json.dumps
class LiveChatController(http.Controller):
def _auth(self, db):
reg = openerp.modules.registry.RegistryManager.get(db)
uid = request.uid
return reg, uid
@http.route('/im_livechat/loader', auth="public")
def loader(self, **kwargs):
p = json.loads(kwargs["p"])
db = p["db"]
channel = p["channel"]
user_name = p.get("user_name", None)
reg, uid = self._auth(db)
with reg.cursor() as cr:
info = reg.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel)
info["db"] = db
info["channel"] = channel
info["userName"] = user_name
return request.make_response(env.get_template("loader.js").render(info),
headers=[('Content-Type', "text/javascript")])
@http.route('/im_livechat/web_page', auth="public")
def web_page(self, **kwargs):
p = json.loads(kwargs["p"])
db = p["db"]
channel = p["channel"]
reg, uid = self._auth(db)
with reg.cursor() as cr:
script = reg.get('im_livechat.channel').read(cr, uid, channel, ["script"])["script"]
info = reg.get('im_livechat.channel').get_info_for_chat_src(cr, uid, channel)
info["script"] = script
return request.make_response(env.get_template("web_page.html").render(info),
headers=[('Content-Type', "text/html")])
@http.route('/im_livechat/available', type='json', auth="public")
def available(self, db, channel):
reg, uid = self._auth(db)
with reg.cursor() as cr:
return len(reg.get('im_livechat.channel').get_available_users(cr, uid, channel)) > 0
class im_livechat_channel(osv.osv):
class im_livechat_channel(osv.Model):
_name = 'im_livechat.channel'
def _get_default_image(self, cr, uid, context=None):
@ -92,7 +42,6 @@ class im_livechat_channel(osv.osv):
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):
@ -103,31 +52,45 @@ class im_livechat_channel(osv.osv):
break
return res
def _script(self, cr, uid, ids, name, arg, context=None):
def _script_external(self, cr, uid, ids, name, arg, context=None):
values = {
"url": self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url'),
"dbname":cr.dbname
}
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = env.get_template("include.html").render({
"url": self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url'),
"parameters": {"db":cr.dbname, "channel":record.id},
})
values["channel"] = record.id
res[record.id] = self.pool['ir.ui.view'].render(cr, uid, 'im_livechat.external_loader', values, context=context)
return res
def _script_internal(self, cr, uid, ids, name, arg, context=None):
values = {
"url": self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url'),
"dbname":cr.dbname
}
res = {}
for record in self.browse(cr, uid, ids, context=context):
values["channel"] = record.id
res[record.id] = self.pool['ir.ui.view'].render(cr, uid, 'im_livechat.internal_loader', values, context=context)
return res
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, uid, 'web.base.url') + \
"/im_livechat/web_page?p=" + json.dumps({"db":cr.dbname, "channel":record.id})
"/im_livechat/support/%s/%i" % (cr.dbname, record.id)
return res
_columns = {
'name': fields.char(string="Channel Name", required=True),
'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': fields.function(_script, type='text', string='Script', 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='url', string='Web Page', store=False, size="200"),
'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."),
'button_text': fields.char(string="Text of the Button", size=200),
'input_placeholder': fields.char(string="Chat Input Placeholder", size=200),
'default_message': fields.char(string="Welcome Message", size=200, 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."),
@ -161,24 +124,25 @@ class im_livechat_channel(osv.osv):
}
def get_available_users(self, cr, uid, channel_id, context=None):
channel = self.browse(cr, openerp.SUPERUSER_ID, channel_id, context=context)
im_user_ids = self.pool.get("im.user").search(cr, uid, [["user_id", "in", [user.id for user in channel.user_ids]]], context=context)
""" get available user of a given channel """
channel = self.browse(cr, uid, channel_id, context=context)
users = []
for iuid in im_user_ids:
imuser = self.pool.get("im.user").browse(cr, uid, iuid, context=context)
if imuser.im_status:
users.append(imuser)
for user_id in channel.user_ids:
if (user_id.im_status == 'online'):
users.append(user_id)
return users
def get_session(self, cr, uid, channel_id, uuid, context=None):
self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
users = self.get_available_users(cr, openerp.SUPERUSER_ID, channel_id, context=context)
def get_channel_session(self, cr, uid, channel_id, anonymous_name, context=None):
""" 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, uid, channel_id, context=context)
if len(users) == 0:
return False
user_id = random.choice(users).id
session = self.pool.get("im.session").session_get(cr, uid, [user_id], uuid, context=context)
self.pool.get("im.session").write(cr, openerp.SUPERUSER_ID, session.get("id"), {'channel_id': channel_id}, context=context)
return session.get("id")
# create the session, and add the link with the given channel
Session = self.pool["im_chat.session"]
newid = Session.create(cr, uid, {'user_ids': [(4, user_id)], 'channel_id': channel_id, 'anonymous_name' : anonymous_name}, context=context)
return Session.session_info(cr, uid, [newid], context=context)
def test_channel(self, cr, uid, channel, context=None):
if not channel:
@ -189,7 +153,7 @@ class im_livechat_channel(osv.osv):
}
def get_info_for_chat_src(self, cr, uid, channel, context=None):
url = self.pool.get('ir.config_parameter').get_param(cr, openerp.SUPERUSER_ID, 'web.base.url')
url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
chan = self.browse(cr, uid, channel, context=context)
return {
"url": url,
@ -207,9 +171,69 @@ class im_livechat_channel(osv.osv):
self.write(cr, uid, ids, {'user_ids': [(3, uid)]})
return True
class im_session(osv.osv):
_inherit = 'im.session'
class im_chat_session(osv.Model):
_inherit = 'im_chat.session'
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'),
'channel_id': fields.many2one("im_livechat.channel", "Channel"),
'fullname' : fields.function(_get_fullname, type="char", string="Complete name"),
}
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
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)
info["dbname"] = dbname
info["channel"] = channel_id
info["channel_name"] = registry.get('im_livechat.channel').read(cr, uid, channel_id, ['name'], context=context)["name"]
return request.render('im_livechat.support_page', info)
@http.route('/im_livechat/loader/<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["dbname"] = dbname
info["channel"] = channel_id
info["username"] = kwargs.get("username", "Visitor")
return request.render('im_livechat.loader', info)
@http.route('/im_livechat/get_session', type="json", auth="none")
def get_session(self, channel_id, anonymous_name):
cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db
reg = openerp.modules.registry.RegistryManager.get(db)
# if geoip, add the country name to the anonymous name
if hasattr(request, 'geoip'):
anonymous_name = anonymous_name + " ("+request.geoip.get('country_name', "")+")"
return reg.get("im_livechat.channel").get_channel_session(cr, uid, channel_id, anonymous_name, context=context)
@http.route('/im_livechat/available', type='json', auth="none")
def available(self, db, channel):
cr, uid, context, db = request.cr, request.uid or openerp.SUPERUSER_ID, request.context, request.db
reg = openerp.modules.registry.RegistryManager.get(db)
with reg.cursor() as cr:
return len(reg.get('im_livechat.channel').get_available_users(cr, uid, channel)) > 0

View File

@ -1,2 +0,0 @@
<script type="text/javascript" src="{{url}}/im_livechat/static/ext/static/lib/requirejs/require.js"></script>
<script type="text/javascript" src='{{url}}/im_livechat/loader?p={{parameters | json | escape}}'></script>

View File

@ -1,47 +0,0 @@
(function() {
var tmpQWeb2 = window.QWeb2;
require.config({
context: "oelivesupport",
baseUrl: {{url | json}},
paths: {
jquery: "im_livechat/static/ext/static/lib/jquery/jquery",
underscore: "im_livechat/static/ext/static/lib/underscore/underscore",
qweb2: "im_livechat/static/ext/static/lib/qweb/qweb2",
openerp: "web/static/src/js/openerpframework",
"jquery.achtung": "im_livechat/static/ext/static/lib/jquery-achtung/src/ui.achtung",
livesupport: "im_livechat/static/ext/static/js/livesupport",
im_common: "im/static/src/js/im_common"
},
shim: {
underscore: {
init: function() {
return _.noConflict();
},
},
qweb2: {
init: function() {
var QWeb2 = window.QWeb2;
window.QWeb2 = tmpQWeb2;
return QWeb2;
},
},
"jquery.achtung": {
deps: ['jquery'],
},
},
})(["livesupport", "jquery"], function(livesupport, jQuery) {
jQuery.noConflict();
console.log("loaded live support");
livesupport.main({{url | json}}, {{db | json}}, "public", "public", {{channel | json}}, {
buttonText: {{buttonText | json}},
inputPlaceholder: {{inputPlaceholder | json}},
defaultMessage: {{(defaultMessage or None) | json}},
auto: window.oe_im_livechat_auto || false,
userName: {{userName | json}} || undefined,
});
});
})();

View File

@ -23,9 +23,9 @@
<record id="message_rule_1" model="ir.rule">
<field name="name">Live Support Managers can read messages from live support</field>
<field name="model_id" ref="im.model_im_message"/>
<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="domain_force">[('session_id.channel_id', '!=', None)]</field>
<field name="domain_force">[('to_id.channel_id', '!=', None)]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>

View File

@ -2,5 +2,3 @@ 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_im_user_portal,im_livechat.im.user.portal,im.model_im_user,base.group_portal,1,0,0,0
access_im_user,im_livechat.im.user,im.model_im_user,base.group_public,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ls_chann1 im_livechat.channel model_im_livechat_channel 1 0 0 0
3 access_ls_chann2 im_livechat.channel model_im_livechat_channel group_im_livechat 1 1 1 0
4 access_ls_chann3 im_livechat.channel model_im_livechat_channel group_im_livechat_manager 1 1 1 1
access_im_user_portal im_livechat.im.user.portal im.model_im_user base.group_portal 1 0 0 0
access_im_user im_livechat.im.user im.model_im_user base.group_public 1 0 0 0

View File

@ -1,3 +0,0 @@
{
"directory": "static/lib/"
}

View File

@ -1,20 +0,0 @@
module.exports = function(grunt) {
grunt.initConfig({
jshint: {
src: ['static/js/*.js'],
options: {
sub: true, //[] instead of .
evil: true, //eval
laxbreak: true, //unsafe line breaks
},
},
});
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.registerTask('test', []);
grunt.registerTask('default', ['jshint']);
};

View File

@ -1,3 +0,0 @@
static/js/livesupport_templates.js: static/js/livesupport_templates.html
python static/js/to_jsonp.py static/js/livesupport_templates.html oe_livesupport_templates_callback > static/js/livesupport_templates.js

View File

@ -1,18 +0,0 @@
{
"name": "im_livechat",
"version": "0.0.0",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": "1.8.3",
"underscore": "1.3.1",
"qweb": "git@github.com:OpenERP/qweb.git#~1.0.0",
"jquery-achtung": "git://github.com/joshvarner/jquery-achtung.git",
"requirejs": "~2.1.8"
}
}

View File

@ -1,6 +0,0 @@
{
"devDependencies": {
"grunt": "*",
"grunt-contrib-jshint": "*"
}
}

View File

@ -1,125 +0,0 @@
/*
This file must compile in EcmaScript 3 and work in IE7.
*/
define(["openerp", "im_common", "underscore", "require", "jquery",
"jquery.achtung"], function(openerp, im_common, _, require, $) {
/* jshint es3: true */
"use strict";
var _t = openerp._t;
var livesupport = {};
livesupport.main = function(server_url, db, login, password, channel, options) {
options = options || {};
_.defaults(options, {
buttonText: _t("Chat with one of our collaborators"),
inputPlaceholder: _t("How may I help you?"),
defaultMessage: null,
auto: false,
userName: _t("Anonymous"),
anonymous_mode: true
});
im_common.notification = notification;
console.log("starting live support customer app");
im_common.connection = new openerp.Session(null, server_url, { override_session: true });
return im_common.connection.session_authenticate(db, login, password).then(function() {
var defs = [];
defs.push(add_css("/im/static/src/css/im_common.css"));
defs.push(add_css("/im_livechat/static/ext/static/lib/jquery-achtung/src/ui.achtung.css"));
defs.push(im_common.connection.rpc('/web/proxy/load', {path: '/im_livechat/static/ext/static/js/livechat.xml'}).then(function(xml) {
openerp.qweb.add_template(xml);
}));
defs.push(im_common.connection.rpc('/web/proxy/load', {path: '/im/static/src/xml/im_common.xml'}).then(function(xml) {
openerp.qweb.add_template(xml);
}));
return $.when.apply($, defs);
}).then(function() {
return im_common.connection.rpc("/im_livechat/available", {db: db, channel: channel}).then(function(activated) {
if (! activated & ! options.auto)
return;
var button = new im_common.ChatButton(null, channel, options);
button.appendTo($("body"));
if (options.auto)
button.click();
});
});
};
var add_css = function(relative_file_name) {
var css_def = $.Deferred();
$('<link rel="stylesheet" href="' + im_common.connection.url(relative_file_name) + '"></link>')
.appendTo($("head")).ready(function() {
css_def.resolve();
});
return css_def.promise();
};
var notification = function(message) {
$.achtung({message: message, timeout: 0, showEffects: false, hideEffects: false});
};
im_common.ChatButton = openerp.Widget.extend({
className: "openerp_style oe_chat_button",
events: {
"click": "click"
},
init: function(parent, channel, options) {
this._super(parent);
this.channel = channel;
this.options = options;
this.text = options.buttonText;
},
start: function() {
this.$().append(openerp.qweb.render("chatButton", {widget: this}));
},
click: function() {
if (! this.manager) {
this.manager = new im_common.ConversationManager(this, this.options);
this.manager.set("bottom_offset", 37);
this.activated_def = this.manager.start_polling();
}
var def = $.Deferred();
$.when(this.activated_def).then(function() {
def.resolve();
}, function() {
def.reject();
});
setTimeout(function() {
def.reject();
}, 5000);
return def.then(_.bind(this.chat, this), function() {
im_common.notification(_t("It seems the connection to the server is encountering problems, please try again later."));
});
},
chat: function() {
var self = this;
if (this.manager.conversations.length > 0)
return;
im_common.connection.model("im_livechat.channel").call("get_session", [this.channel, this.manager.me.get("uuid")]).then(function(session_id) {
if (! session_id) {
im_common.notification(_t("None of our collaborators seems to be available, please try again later."));
return;
}
self.manager.activate_session(session_id, true).then(function(conv) {
if (self.options.defaultMessage) {
setTimeout(function(){
conv.received_message({
message: self.options.defaultMessage,
date: openerp.datetime_to_str(new Date()),
from_id: [conv.get("users")[0].get("id"), "Unknown"]
});
},
2500);
}
});
});
}
});
return livesupport;
});

View File

@ -1,346 +0,0 @@
/**
* achtung %%VERSION%%
*
* Growl-like notifications for jQuery
*
* Copyright (c) 2009 Josh Varner <josh@voxwerk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Portions of this file are from the jQuery UI CSS framework.
*
* @license http://www.opensource.org/licenses/mit-license.php
* @author Josh Varner <josh@voxwerk.com>
*/
#achtung-overlay {
overflow: hidden;
position: fixed;
top: 0;
right: 0;
z-index: 50;
}
/* IE 6 doesn't support position: fixed */
* html #achtung-overlay {
position: absolute;
}
/* IE6 includes padding in width */
* html .achtung {
width: 260px;
}
#achtung-wrapper {
overflow: hidden;
padding: 25px 30px 10px 10px;
}
.achtung {
display: none;
float: right;
clear: right;
margin-bottom: 8px;
padding: 15px;
background: #000;
background: rgba(0,0,0,.95);
color: white;
width: 230px;
font-weight: normal;
font-style: italic;
font-size: 1.05em;
position: relative;
overflow: hidden;
text-shadow: -1px -1px 0 rgba(0,0,0,.3);
box-shadow: rgba(0,0,0,.3) 0 1px 4px;
-moz-box-shadow: rgba(0,0,0,.3) 0 1px 4px;
-webkit-box-shadow: rgba(0,0,0,.3) 0 1px 4px;
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
opacity: 1.0;
filter:Alpha(Opacity=100);
}
.achtung-default {
background: #000;
background: -moz-linear-gradient(top, rgba(150,150,150,.9), rgba(120,120,120,.9) 70%);
background: -webkit-gradient(linear, left top, left bottom,
from(rgba(150,150,150,.9)),
color-stop(70%, rgba(120,120,120,.9)),
to(rgba(120,120,120,.9))
) no-repeat;
color: white;
}
.achtung .achtung-message-icon {
float: left;
margin: 0 .8em 0 -.5em;
zoom: 1;
}
.achtung .ui-icon.achtung-close-button {
overflow: hidden;
float: right;
position: relative;
top: -8px;
right: -8px;
cursor: pointer;
background-image: url('images/ui-icons_cccccc_256x240.png');
}
.achtung .ui-icon.achtung-close-button:hover {
background-image: url('images/ui-icons_ffffff_256x240.png');
}
/* Slightly darker for these colors (readability) */
.achtungSuccess, .achtungFail, .achtungWait {
/* Note that if using show/hide animations, IE will lose
this setting */
opacity: 1.0;
filter:Alpha(Opacity=100);
}
.achtungSuccess {
background: #6a5;
background: #6a5 -moz-linear-gradient(top, #8c7, #6a5 70%);
background: #6a5 -webkit-gradient(linear, left top, left bottom,
from(#8c7),
color-stop(70%, #6a5),
to(#6a5)
) no-repeat;
}
.achtungFail {
background: #a55;
background: #a55 -moz-linear-gradient(top, #c66, #a44 70%);
background: #789 -webkit-gradient(linear, left top, left bottom,
from(#c66),
color-stop(70%, #a44),
to(#a44)
) no-repeat;
}
.achtungWait {
background: #789;
background: #789 -moz-linear-gradient(top, #89a, #678 70%);
background: #789 -webkit-gradient(linear, left top, left bottom,
from(#89a),
color-stop(70%, #678),
to(#678)
) no-repeat;
}
.achtungSuccess .ui-icon.achtung-close-button,
.achtungFail .ui-icon.achtung-close-button {
background-image: url('images/ui-icons_444444_256x240.png');
}
.achtungSuccess .ui-icon.achtung-close-button:hover,
.achtungFail .ui-icon.achtung-close-button:hover {
background-image: url('images/ui-icons_ffffff_256x240.png');
}
.achtung .wait-icon {
background-image: url('images/wait.gif');
}
.achtung .achtung-message {
display: inline;
}
/**
* This section from jQuery UI CSS framework
* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
* Can (and should) be removed if you are already loading the jQuery UI CSS
* to reduce payload size.
*/
.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
.ui-icon { width: 16px; height: 16px; background-image: url('images/ui-icons_222222_256x240.png'); }
.ui-icon-carat-1-n { background-position: 0 0; }
.ui-icon-carat-1-ne { background-position: -16px 0; }
.ui-icon-carat-1-e { background-position: -32px 0; }
.ui-icon-carat-1-se { background-position: -48px 0; }
.ui-icon-carat-1-s { background-position: -64px 0; }
.ui-icon-carat-1-sw { background-position: -80px 0; }
.ui-icon-carat-1-w { background-position: -96px 0; }
.ui-icon-carat-1-nw { background-position: -112px 0; }
.ui-icon-carat-2-n-s { background-position: -128px 0; }
.ui-icon-carat-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -64px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -64px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-off { background-position: -96px -144px; }
.ui-icon-radio-on { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }

View File

@ -1,282 +0,0 @@
/**
* achtung %%VERSION%%
*
* Growl-like notifications for jQuery
*
* Copyright (c) 2009 Josh Varner <josh@voxwerk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @license http://www.opensource.org/licenses/mit-license.php
* @author Josh Varner <josh@voxwerk.com>
*/
/*jslint browser:true, white:false, onevar:false, nomen:false, bitwise:false, plusplus:false, immed: false */
/*globals window, jQuery */
(function ($) {
var widgetName = 'achtung';
/**
* This is based on the jQuery UI $.widget code. I would have just made this
* a $.widget but I didn't want the jQuery UI dependency.
*/
$.fn.achtung = function (options) {
var isMethodCall = (typeof options === 'string'),
args = Array.prototype.slice.call(arguments, isMethodCall ? 1 : 0);
// handle initialization and non-getter methods
return this.each(function () {
// prevent calls to internal methods
if (isMethodCall && options.substring(0, 1) === '_') {
return;
}
var instance = $.data(this, widgetName);
// constructor
if (!instance && !isMethodCall) {
$.data(this, widgetName, new $.achtung(this))._init(args);
}
if (!!instance && isMethodCall && $.isFunction(instance[options])) {
instance[options].apply(instance, args);
}
});
};
$.achtung = function (element) {
if (!element || !element.nodeType) {
var el = $('<div>');
return el.achtung.apply(el, arguments);
}
this.container = $(element);
};
/**
* Static members
**/
$.extend($.achtung, {
version: '%%VERSION%%',
overlay: false,
wrapper: false,
defaults: {
timeout: 10,
disableClose: false,
icon: false,
className: 'achtung-default',
crossFadeMessage: 500, // 0 to disable
animateClassSwitch: 0, // 0 to disable (doesn't work with gradient backgrounds)
showEffects: {'opacity':'toggle'}, // ,'height':'toggle'},
hideEffects: {'opacity':'toggle'}, // ,'height':'toggle'},
showEffectDuration: 300,
hideEffectDuration: 500
}
});
/**
* Non-static members
**/
$.extend($.achtung.prototype, {
container: false,
icon: false,
message: false,
closeTimer: false,
options: {},
_init: function (args) {
var o, self = this;
o = this.options = $.extend.apply($, [{}, $.achtung.defaults].concat(args));
if ((o.animateClassSwitch > 0) && !('switchClass' in $.fn)) {
o.animateClassSwitch = this.options.animateClassSwitch = 0;
}
if (!o.disableClose) {
$('<span class="achtung-close-button ui-icon ui-icon-close" />')
.prependTo(this.container)
.bind({
click: function () { self.close(); }
});
}
this.changeIcon(o.icon, true);
if (o.message) {
this.message = $('<span>', {
'class': 'achtung-message',
html: o.message
}).appendTo(this.container);
}
if ('className' in o) {
this.container.addClass(o.className);
}
if ('css' in o) {
this.container.css(o.css);
}
if (!$.achtung.overlay) {
$.achtung.overlay = $('<div id="achtung-overlay"><div id="achtung-wrapper"></div></div>');
$.achtung.overlay.appendTo(document.body);
$.achtung.wrapper = $('#achtung-wrapper');
}
this.container.addClass('achtung').hide().appendTo($.achtung.wrapper);
if (o.showEffects) {
this.container.animate(o.showEffects, o.showEffectDuration);
} else {
this.container.show();
}
this.timeout(o.timeout);
},
timeout: function (timeout) {
var self = this;
if (this.closeTimer) {
clearTimeout(this.closeTimer);
}
if (timeout > 0) {
this.closeTimer = setTimeout(function () { self.close(); }, timeout * 1000);
this.options.timeout = timeout;
} else if (timeout < 0) {
this.close();
}
},
/**
* Change the CSS class associated with this message.
*
* @param newClass string Name of new class to associate
*/
changeClass: function (newClass) {
var oldClass = '' + this.options.className,
self = this;
if (oldClass === newClass) {
return;
}
this.container.queue(function (next) {
if (self.options.animateClassSwitch > 0) {
$(this).switchClass(oldClass, newClass, self.options.animateClassSwitch);
} else {
$(this).removeClass(oldClass).addClass(newClass);
}
next();
});
this.options.className = newClass;
},
changeIcon: function (newIcon, force) {
if (!force && this.options.icon === newIcon) {
return;
}
if (!!this.icon) {
if (newIcon) {
this.icon.removeClass(this.options.icon).addClass(newIcon);
} else {
this.icon.remove();
this.icon = false;
}
} else if (newIcon) {
this.icon = $('<span class="achtung-message-icon ui-icon ' + newIcon + '" />');
this.container.prepend(this.icon);
}
this.options.icon = newIcon;
},
changeMessage: function (newMessage) {
if (this.options.crossFadeMessage > 0) {
this.message.clone()
.css('position', 'absolute')
.insertBefore(this.message)
.fadeOut(this.options.crossFadeMessage, function () { $(this).remove(); });
this.message.hide().html(newMessage).fadeIn(this.options.crossFadeMessage);
} else {
this.message.html(newMessage);
}
this.options.message = newMessage;
},
update: function () {
var options = $.extend.apply($, [{}].concat(Array.prototype.slice.call(arguments, 0))),
map = {
className: 'changeClass',
css: 'css',
icon: 'changeIcon',
message: 'changeMessage',
timeout: 'timeout'
};
for (var prop in map) {
if (prop in options) {
this[map[prop]](options[prop]);
}
}
},
isVisible: function () {
return (true === this.container.is(':visible'));
},
_trigger: function (type, data) {
this.container.trigger(widgetName + type, data);
},
close: function () {
var o = this.options, self = this;
this._trigger('close');
if (o.hideEffects) {
this.container.animate(o.hideEffects, o.hideEffectDuration, function () {
self.remove();
});
} else {
this.container.hide();
this.remove();
}
},
remove: function () {
this.container.remove();
if ($.achtung.wrapper && !($.achtung.wrapper.contents().length)) {
$.achtung.wrapper = false;
$.achtung.overlay.remove();
$.achtung.overlay = false;
}
}
});
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@ -1,736 +0,0 @@
/*
Copyright (c) 2013, Fabien Meghazi
Released under the MIT license
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// TODO: trim support
// TODO: line number -> https://bugzilla.mozilla.org/show_bug.cgi?id=618650
// TODO: templates orverwritten could be called by t-call="__super__" ?
// TODO: t-set + t-value + children node == scoped variable ?
(function() {
var QWeb2 = {
expressions_cache: {},
RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date'.split(','),
ACTIONS_PRECEDENCE: 'foreach,if,call,set,esc,escf,raw,rawf,js,debug,log'.split(','),
WORD_REPLACEMENT: {
'and': '&&',
'or': '||',
'gt': '>',
'gte': '>=',
'lt': '<',
'lte': '<='
},
tools: {
exception: function(message, context) {
context = context || {};
var prefix = 'QWeb2';
if (context.template) {
prefix += " - template['" + context.template + "']";
}
throw new Error(prefix + ": " + message);
},
warning : function(message) {
if (typeof(window) !== 'undefined' && window.console) {
window.console.warn(message);
}
},
trim: function(s, mode) {
switch (mode) {
case "left":
return s.replace(/^\s*/, "");
case "right":
return s.replace(/\s*$/, "");
default:
return s.replace(/^\s*|\s*$/g, "");
}
},
js_escape: function(s, noquotes) {
return (noquotes ? '' : "'") + s.replace(/\r?\n/g, "\\n").replace(/'/g, "\\'") + (noquotes ? '' : "'");
},
html_escape: function(s, attribute) {
if (s == null) {
return '';
}
s = String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
if (attribute) {
s = s.replace(/"/g, '&quot;');
}
return s;
},
gen_attribute: function(o) {
if (o !== null && o !== undefined) {
if (o.constructor === Array) {
if (o[1] !== null && o[1] !== undefined) {
return this.format_attribute(o[0], o[1]);
}
} else if (typeof o === 'object') {
var r = '';
for (var k in o) {
if (o.hasOwnProperty(k)) {
r += this.gen_attribute([k, o[k]]);
}
}
return r;
}
}
return '';
},
format_attribute: function(name, value) {
return ' ' + name + '="' + this.html_escape(value, true) + '"';
},
extend: function(dst, src, exclude) {
for (var p in src) {
if (src.hasOwnProperty(p) && !(exclude && this.arrayIndexOf(exclude, p) !== -1)) {
dst[p] = src[p];
}
}
return dst;
},
arrayIndexOf : function(array, item) {
for (var i = 0, ilen = array.length; i < ilen; i++) {
if (array[i] === item) {
return i;
}
}
return -1;
},
xml_node_to_string : function(node, childs_only) {
if (childs_only) {
var childs = node.childNodes, r = [];
for (var i = 0, ilen = childs.length; i < ilen; i++) {
r.push(this.xml_node_to_string(childs[i]));
}
return r.join('');
} else {
if (typeof XMLSerializer !== 'undefined') {
return (new XMLSerializer()).serializeToString(node);
} else {
switch(node.nodeType) {
case 1: return node.outerHTML;
case 3: return node.data;
case 4: return '<![CDATA[' + node.data + ']]>';
case 8: return '<!-- ' + node.data + '-->';
}
throw new Error('Unknown node type ' + node.nodeType);
}
}
},
call: function(context, template, old_dict, _import, callback) {
var new_dict = this.extend({}, old_dict);
new_dict['__caller__'] = old_dict['__template__'];
if (callback) {
new_dict['__content__'] = callback(context, new_dict);
}
var r = context.engine._render(template, new_dict);
if (_import) {
if (_import === '*') {
this.extend(old_dict, new_dict, ['__caller__', '__template__']);
} else {
_import = _import.split(',');
for (var i = 0, ilen = _import.length; i < ilen; i++) {
var v = _import[i];
old_dict[v] = new_dict[v];
}
}
}
return r;
},
foreach: function(context, enu, as, old_dict, callback) {
if (enu != null) {
var size, new_dict = this.extend({}, old_dict);
new_dict[as + "_all"] = enu;
var as_value = as + "_value",
as_index = as + "_index",
as_first = as + "_first",
as_last = as + "_last",
as_parity = as + "_parity";
if (size = enu.length) {
new_dict[as + "_size"] = size;
for (var j = 0, jlen = enu.length; j < jlen; j++) {
var cur = enu[j];
new_dict[as_value] = cur;
new_dict[as_index] = j;
new_dict[as_first] = j === 0;
new_dict[as_last] = j + 1 === size;
new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even');
if (cur.constructor === Object) {
this.extend(new_dict, cur);
}
new_dict[as] = cur;
callback(context, new_dict);
}
} else if (enu.constructor == Number) {
var _enu = [];
for (var i = 0; i < enu; i++) {
_enu.push(i);
}
this.foreach(context, _enu, as, old_dict, callback);
} else {
var index = 0;
for (var k in enu) {
if (enu.hasOwnProperty(k)) {
var v = enu[k];
new_dict[as_value] = v;
new_dict[as_index] = index;
new_dict[as_first] = index === 0;
new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even');
new_dict[as] = k;
callback(context, new_dict);
index += 1;
}
}
}
} else {
this.exception("No enumerator given to foreach", context);
}
}
}
};
QWeb2.Engine = (function() {
function Engine() {
// TODO: handle prefix at template level : t-prefix="x", don't forget to lowercase it
this.prefix = 't';
this.debug = false;
this.templates_resources = []; // TODO: implement this.reload()
this.templates = {};
this.compiled_templates = {};
this.extend_templates = {};
this.default_dict = {};
this.tools = QWeb2.tools;
this.jQuery = window.jQuery;
this.reserved_words = QWeb2.RESERVED_WORDS.slice(0);
this.actions_precedence = QWeb2.ACTIONS_PRECEDENCE.slice(0);
this.word_replacement = QWeb2.tools.extend({}, QWeb2.WORD_REPLACEMENT);
this.preprocess_node = null;
for (var i = 0; i < arguments.length; i++) {
this.add_template(arguments[i]);
}
}
QWeb2.tools.extend(Engine.prototype, {
add_template : function(template) {
this.templates_resources.push(template);
if (template.constructor === String) {
template = this.load_xml(template);
}
var ec = (template.documentElement && template.documentElement.childNodes) || template.childNodes || [];
for (var i = 0; i < ec.length; i++) {
var node = ec[i];
if (node.nodeType === 1) {
if (node.nodeName == 'parsererror') {
return this.tools.exception(node.innerText);
}
var name = node.getAttribute(this.prefix + '-name');
var extend = node.getAttribute(this.prefix + '-extend');
if (name && extend) {
// Clone template and extend it
if (!this.templates[extend]) {
return this.tools.exception("Can't clone undefined template " + extend);
}
this.templates[name] = this.templates[extend].cloneNode(true);
extend = name;
name = undefined;
}
if (name) {
this.templates[name] = node;
this.compiled_templates[name] = null;
} else if (extend) {
delete(this.compiled_templates[extend]);
if (this.extend_templates[extend]) {
this.extend_templates[extend].push(node);
} else {
this.extend_templates[extend] = [node];
}
}
}
}
return true;
},
load_xml : function(s) {
s = this.tools.trim(s);
if (s.charAt(0) === '<') {
return this.load_xml_string(s);
} else {
var req = this.get_xhr();
if (req) {
// TODO: third parameter is async : https://developer.mozilla.org/en/XMLHttpRequest#open()
// do an on_ready in QWeb2{} that could be passed to add_template
if (this.debug) {
s += '?debug=' + (new Date()).getTime(); // TODO fme: do it properly in case there's already url parameters
}
req.open('GET', s, false);
req.send(null);
var xDoc = req.responseXML;
if (xDoc) {
if (!xDoc.documentElement) {
throw new Error("QWeb2: This xml document has no root document : " + xDoc.responseText);
}
if (xDoc.documentElement.nodeName == "parsererror") {
return this.tools.exception(xDoc.documentElement.childNodes[0].nodeValue);
}
return xDoc;
} else {
return this.load_xml_string(req.responseText);
}
}
}
},
load_xml_string : function(s) {
if (window.DOMParser) {
var dp = new DOMParser();
var r = dp.parseFromString(s, "text/xml");
if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
return this.tools.exception(r.body.innerText);
}
return r;
}
var xDoc;
try {
// new ActiveXObject("Msxml2.DOMDocument.4.0");
xDoc = new ActiveXObject("MSXML2.DOMDocument");
} catch (e) {
return this.tools.exception(
"Could not find a DOM Parser: " + e.message);
}
xDoc.async = false;
xDoc.preserveWhiteSpace = true;
xDoc.loadXML(s);
return xDoc;
},
has_template : function(template) {
return !!this.templates[template];
},
get_xhr : function() {
if (window.XMLHttpRequest) {
return new window.XMLHttpRequest();
}
try {
return new ActiveXObject('MSXML2.XMLHTTP.3.0');
} catch (e) {
return null;
}
},
compile : function(node) {
var e = new QWeb2.Element(this, node);
var template = node.getAttribute(this.prefix + '-name');
return " /* 'this' refers to Qweb2.Engine instance */\n" +
" var context = { engine : this, template : " + (this.tools.js_escape(template)) + " };\n" +
" dict = dict || {};\n" +
" dict['__template__'] = '" + template + "';\n" +
" var r = [];\n" +
" /* START TEMPLATE */" +
(this.debug ? "" : " try {\n") +
(e.compile()) + "\n" +
" /* END OF TEMPLATE */" +
(this.debug ? "" : " } catch(error) {\n" +
" if (console && console.exception) console.exception(error);\n" +
" context.engine.tools.exception('Runtime Error: ' + error, context);\n") +
(this.debug ? "" : " }\n") +
" return r.join('');";
},
render : function(template, dict) {
dict = dict || {};
QWeb2.tools.extend(dict, this.default_dict);
/*if (this.debug && window['console'] !== undefined) {
console.time("QWeb render template " + template);
}*/
var r = this._render(template, dict);
/*if (this.debug && window['console'] !== undefined) {
console.timeEnd("QWeb render template " + template);
}*/
return r;
},
_render : function(template, dict) {
if (this.compiled_templates[template]) {
return this.compiled_templates[template].apply(this, [dict || {}]);
} else if (this.templates[template]) {
var ext;
if (ext = this.extend_templates[template]) {
var extend_node;
while (extend_node = ext.shift()) {
this.extend(template, extend_node);
}
}
var code = this.compile(this.templates[template]), tcompiled;
try {
tcompiled = new Function(['dict'], code);
} catch (error) {
if (this.debug && window.console) {
console.log(code);
}
this.tools.exception("Error evaluating template: " + error, { template: name });
}
if (!tcompiled) {
this.tools.exception("Error evaluating template: (IE?)" + error, { template: name });
}
this.compiled_templates[template] = tcompiled;
return this.render(template, dict);
} else {
return this.tools.exception("Template '" + template + "' not found");
}
},
extend : function(template, extend_node) {
if (!this.jQuery) {
return this.tools.exception("Can't extend template " + template + " without jQuery");
}
var template_dest = this.templates[template];
for (var i = 0, ilen = extend_node.childNodes.length; i < ilen; i++) {
var child = extend_node.childNodes[i];
if (child.nodeType === 1) {
var jquery = child.getAttribute(this.prefix + '-jquery'),
operation = child.getAttribute(this.prefix + '-operation'),
target,
error_msg = "Error while extending template '" + template;
if (jquery) {
target = this.jQuery(jquery, template_dest);
} else {
this.tools.exception(error_msg + "No expression given");
}
error_msg += "' (expression='" + jquery + "') : ";
if (operation) {
var allowed_operations = "append,prepend,before,after,replace,inner".split(',');
if (this.tools.arrayIndexOf(allowed_operations, operation) == -1) {
this.tools.exception(error_msg + "Invalid operation : '" + operation + "'");
}
operation = {'replace' : 'replaceWith', 'inner' : 'html'}[operation] || operation;
target[operation](child.cloneNode(true).childNodes);
} else {
try {
var f = new Function(['$', 'document'], this.tools.xml_node_to_string(child, true));
} catch(error) {
return this.tools.exception("Parse " + error_msg + error);
}
try {
f.apply(target, [this.jQuery, template_dest.ownerDocument]);
} catch(error) {
return this.tools.exception("Runtime " + error_msg + error);
}
}
}
}
}
});
return Engine;
})();
QWeb2.Element = (function() {
function Element(engine, node) {
this.engine = engine;
this.node = node;
this.tag = node.tagName;
this.actions = {};
this.actions_done = [];
this.attributes = {};
this.children = [];
this._top = [];
this._bottom = [];
this._indent = 1;
this.process_children = true;
var childs = this.node.childNodes;
if (childs) {
for (var i = 0, ilen = childs.length; i < ilen; i++) {
this.children.push(new QWeb2.Element(this.engine, childs[i]));
}
}
var attrs = this.node.attributes;
if (attrs) {
for (var j = 0, jlen = attrs.length; j < jlen; j++) {
var attr = attrs[j];
var name = attr.name;
var m = name.match(new RegExp("^" + this.engine.prefix + "-(.+)"));
if (m) {
name = m[1];
if (name === 'name') {
continue;
}
this.actions[name] = attr.value;
} else {
this.attributes[name] = attr.value;
}
}
}
if (this.engine.preprocess_node) {
this.engine.preprocess_node.call(this);
}
}
QWeb2.tools.extend(Element.prototype, {
compile : function() {
var r = [],
instring = false,
lines = this._compile().split('\n');
for (var i = 0, ilen = lines.length; i < ilen; i++) {
var m, line = lines[i];
if (m = line.match(/^(\s*)\/\/@string=(.*)/)) {
if (instring) {
if (this.engine.debug) {
// Split string lines in indented r.push arguments
r.push((m[2].indexOf("\\n") != -1 ? "',\n\t" + m[1] + "'" : '') + m[2]);
} else {
r.push(m[2]);
}
} else {
r.push(m[1] + "r.push('" + m[2]);
instring = true;
}
} else {
if (instring) {
r.push("');\n");
}
instring = false;
r.push(line + '\n');
}
}
return r.join('');
},
_compile : function() {
switch (this.node.nodeType) {
case 3:
case 4:
this.top_string(this.node.data);
break;
case 1:
this.compile_element();
}
var r = this._top.join('');
if (this.process_children) {
for (var i = 0, ilen = this.children.length; i < ilen; i++) {
var child = this.children[i];
child._indent = this._indent;
r += child._compile();
}
}
r += this._bottom.join('');
return r;
},
format_expression : function(e) {
/* Naive format expression builder. Replace reserved words and variables to dict[variable]
* Does not handle spaces before dot yet, and causes problems for anonymous functions. Use t-js="" for that */
if (QWeb2.expressions_cache[e]) {
return QWeb2.expressions_cache[e];
}
var chars = e.split(''),
instring = '',
invar = '',
invar_pos = 0,
r = '';
chars.push(' ');
for (var i = 0, ilen = chars.length; i < ilen; i++) {
var c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = '';
}
} else if (c === '"' || c === "'") {
instring = c;
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
invar = c;
invar_pos = i;
continue;
} else if (c.match(/\W/) && invar.length) {
// TODO: Should check for possible spaces before dot
if (chars[invar_pos - 1] !== '.' && QWeb2.tools.arrayIndexOf(this.engine.reserved_words, invar) < 0) {
invar = this.engine.word_replacement[invar] || ("dict['" + invar + "']");
}
r += invar;
invar = '';
} else if (invar.length) {
invar += c;
continue;
}
r += c;
}
r = r.slice(0, -1);
QWeb2.expressions_cache[e] = r;
return r;
},
string_interpolation : function(s) {
if (!s) {
return "''";
}
var regex = /^{(.*)}(.*)/,
src = s.split(/#/),
r = [];
for (var i = 0, ilen = src.length; i < ilen; i++) {
var val = src[i],
m = val.match(regex);
if (m) {
r.push("(" + this.format_expression(m[1]) + ")");
if (m[2]) {
r.push(this.engine.tools.js_escape(m[2]));
}
} else if (!(i === 0 && val === '')) {
r.push(this.engine.tools.js_escape((i === 0 ? '' : '#') + val));
}
}
return r.join(' + ');
},
indent : function() {
return this._indent++;
},
dedent : function() {
if (this._indent !== 0) {
return this._indent--;
}
},
get_indent : function() {
return new Array(this._indent + 1).join("\t");
},
top : function(s) {
return this._top.push(this.get_indent() + s + '\n');
},
top_string : function(s) {
return this._top.push(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n');
},
bottom : function(s) {
return this._bottom.unshift(this.get_indent() + s + '\n');
},
bottom_string : function(s) {
return this._bottom.unshift(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n');
},
compile_element : function() {
for (var i = 0, ilen = this.engine.actions_precedence.length; i < ilen; i++) {
var a = this.engine.actions_precedence[i];
if (a in this.actions) {
var value = this.actions[a];
var key = 'compile_action_' + a;
if (this[key]) {
this[key](value);
} else if (this.engine[key]) {
this.engine[key].call(this, value);
} else {
this.engine.tools.exception("No handler method for action '" + a + "'");
}
}
}
if (this.tag.toLowerCase() !== this.engine.prefix) {
var tag = "<" + this.tag;
for (var a in this.attributes) {
tag += this.engine.tools.gen_attribute([a, this.attributes[a]]);
}
this.top_string(tag);
if (this.actions.att) {
this.top("r.push(context.engine.tools.gen_attribute(" + (this.format_expression(this.actions.att)) + "));");
}
for (var a in this.actions) {
var v = this.actions[a];
var m = a.match(/att-(.+)/);
if (m) {
this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.format_expression(v)) + ")]));");
}
var m = a.match(/attf-(.+)/);
if (m) {
this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.string_interpolation(v)) + ")]));");
}
}
if (this.children.length || this.actions.opentag === 'true') {
this.top_string(">");
this.bottom_string("</" + this.tag + ">");
} else {
this.top_string("/>");
}
}
},
compile_action_if : function(value) {
this.top("if (" + (this.format_expression(value)) + ") {");
this.bottom("}");
this.indent();
},
compile_action_foreach : function(value) {
var as = this.actions['as'] || value.replace(/[^a-zA-Z0-9]/g, '_');
//TODO: exception if t-as not valid
this.top("context.engine.tools.foreach(context, " + (this.format_expression(value)) + ", " + (this.engine.tools.js_escape(as)) + ", dict, function(context, dict) {");
this.bottom("});");
this.indent();
},
compile_action_call : function(value) {
var _import = this.actions['import'] || '';
if (this.children.length === 0) {
return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + "));");
} else {
this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + ", function(context, dict) {");
this.bottom("}));");
this.indent();
this.top("var r = [];");
return this.bottom("return r.join('');");
}
},
compile_action_set : function(value) {
var variable = this.format_expression(value);
if (this.actions['value']) {
if (this.children.length) {
this.engine.tools.warning("@set with @value plus node chidren found. Children are ignored.");
}
this.top(variable + " = (" + (this.format_expression(this.actions['value'])) + ");");
this.process_children = false;
} else {
if (this.children.length === 0) {
this.top(variable + " = '';");
} else if (this.children.length === 1 && this.children[0].node.nodeType === 3) {
this.top(variable + " = " + (this.engine.tools.js_escape(this.children[0].node.data)) + ";");
this.process_children = false;
} else {
this.top(variable + " = (function(dict) {");
this.bottom("})(dict);");
this.indent();
this.top("var r = [];");
this.bottom("return r.join('');");
}
}
},
compile_action_esc : function(value) {
this.top("r.push(context.engine.tools.html_escape(" + (this.format_expression(value)) + "));");
},
compile_action_escf : function(value) {
this.top("r.push(context.engine.tools.html_escape(" + (this.string_interpolation(value)) + "));");
},
compile_action_raw : function(value) {
this.top("r.push(" + (this.format_expression(value)) + ");");
},
compile_action_rawf : function(value) {
this.top("r.push(" + (this.string_interpolation(value)) + ");");
},
compile_action_js : function(value) {
this.top("(function(" + value + ") {");
this.bottom("})(dict);");
this.indent();
var lines = this.engine.tools.xml_node_to_string(this.node, true).split(/\r?\n/);
for (var i = 0, ilen = lines.length; i < ilen; i++) {
this.top(lines[i]);
}
this.process_children = false;
},
compile_action_debug : function(value) {
this.top("debugger;");
},
compile_action_log : function(value) {
this.top("console.log(" + this.format_expression(value) + ");");
}
});
return Element;
})();
window.QWeb2 = QWeb2;
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,999 +0,0 @@
// Underscore.js 1.3.1
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function() {
// Baseline setup
// --------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// Save the previous value of the `_` variable.
var previousUnderscore = root._;
// Establish the object that gets returned to break out of a loop iteration.
var breaker = {};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var slice = ArrayProto.slice,
unshift = ArrayProto.unshift,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
nativeForEach = ArrayProto.forEach,
nativeMap = ArrayProto.map,
nativeReduce = ArrayProto.reduce,
nativeReduceRight = ArrayProto.reduceRight,
nativeFilter = ArrayProto.filter,
nativeEvery = ArrayProto.every,
nativeSome = ArrayProto.some,
nativeIndexOf = ArrayProto.indexOf,
nativeLastIndexOf = ArrayProto.lastIndexOf,
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind;
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) { return new wrapper(obj); };
// Export the Underscore object for **Node.js**, with
// backwards-compatibility for the old `require()` API. If we're in
// the browser, add `_` as a global object via a string identifier,
// for Closure Compiler "advanced" mode.
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root['_'] = _;
}
// Current version.
_.VERSION = '1.3.1';
// Collection Functions
// --------------------
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles objects with the built-in `forEach`, arrays, and raw objects.
// Delegates to **ECMAScript 5**'s native `forEach` if available.
var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
// Return the results of applying the iterator to each element.
// Delegates to **ECMAScript 5**'s native `map` if available.
_.map = _.collect = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
if (obj.length === +obj.length) results.length = obj.length;
return results;
};
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial) {
memo = value;
initial = true;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
return memo;
};
// The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = _.toArray(obj).reverse();
if (context && !initial) iterator = _.bind(iterator, context);
return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
};
// Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, iterator, context) {
var result;
any(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) {
result = value;
return true;
}
});
return result;
};
// Return all the elements that pass a truth test.
// Delegates to **ECMAScript 5**'s native `filter` if available.
// Aliased as `select`.
_.filter = _.select = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
each(obj, function(value, index, list) {
if (!iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Determine whether all of the elements match a truth test.
// Delegates to **ECMAScript 5**'s native `every` if available.
// Aliased as `all`.
_.every = _.all = function(obj, iterator, context) {
var result = true;
if (obj == null) return result;
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
each(obj, function(value, index, list) {
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
});
return result;
};
// Determine if at least one element in the object matches a truth test.
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) {
iterator || (iterator = _.identity);
var result = false;
if (obj == null) return result;
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
each(obj, function(value, index, list) {
if (result || (result = iterator.call(context, value, index, list))) return breaker;
});
return !!result;
};
// Determine if a given value is included in the array or object using `===`.
// Aliased as `contains`.
_.include = _.contains = function(obj, target) {
var found = false;
if (obj == null) return found;
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
found = any(obj, function(value) {
return value === target;
});
return found;
};
// Invoke a method (with arguments) on every item in a collection.
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2);
return _.map(obj, function(value) {
return (_.isFunction(method) ? method || value : value[method]).apply(value, args);
});
};
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, function(value){ return value[key]; });
};
// Return the maximum element or (element-based computation).
_.max = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return -Infinity;
var result = {computed : -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return Infinity;
var result = {computed : Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed < result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Shuffle an array.
_.shuffle = function(obj) {
var shuffled = [], rand;
each(obj, function(value, index, list) {
if (index == 0) {
shuffled[0] = value;
} else {
rand = Math.floor(Math.random() * (index + 1));
shuffled[index] = shuffled[rand];
shuffled[rand] = value;
}
});
return shuffled;
};
// Sort the object's values by a criterion produced by an iterator.
_.sortBy = function(obj, iterator, context) {
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
criteria : iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
}), 'value');
};
// Groups the object's values by a criterion. Pass either a string attribute
// to group by, or a function that returns the criterion.
_.groupBy = function(obj, val) {
var result = {};
var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
each(obj, function(value, index) {
var key = iterator(value, index);
(result[key] || (result[key] = [])).push(value);
});
return result;
};
// Use a comparator function to figure out at what index an object should
// be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator) {
iterator || (iterator = _.identity);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >> 1;
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
}
return low;
};
// Safely convert anything iterable into a real, live array.
_.toArray = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
if (_.isArray(iterable)) return slice.call(iterable);
if (_.isArguments(iterable)) return slice.call(iterable);
return _.values(iterable);
};
// Return the number of elements in an object.
_.size = function(obj) {
return _.toArray(obj).length;
};
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
// values in the array. Aliased as `head`. The **guard** check allows it to work
// with `_.map`.
_.first = _.head = function(array, n, guard) {
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
};
// Returns everything but the last entry of the array. Especcialy useful on
// the arguments object. Passing **n** will return all the values in
// the array, excluding the last N. The **guard** check allows it to work with
// `_.map`.
_.initial = function(array, n, guard) {
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
};
// Get the last element of an array. Passing **n** will return the last N
// values in the array. The **guard** check allows it to work with `_.map`.
_.last = function(array, n, guard) {
if ((n != null) && !guard) {
return slice.call(array, Math.max(array.length - n, 0));
} else {
return array[array.length - 1];
}
};
// Returns everything but the first entry of the array. Aliased as `tail`.
// Especially useful on the arguments object. Passing an **index** will return
// the rest of the values in the array from that index onward. The **guard**
// check allows it to work with `_.map`.
_.rest = _.tail = function(array, index, guard) {
return slice.call(array, (index == null) || guard ? 1 : index);
};
// Trim out all falsy values from an array.
_.compact = function(array) {
return _.filter(array, function(value){ return !!value; });
};
// Return a completely flattened version of an array.
_.flatten = function(array, shallow) {
return _.reduce(array, function(memo, value) {
if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
memo[memo.length] = value;
return memo;
}, []);
};
// Return a version of the array that does not contain the specified value(s).
_.without = function(array) {
return _.difference(array, slice.call(arguments, 1));
};
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iterator) {
var initial = iterator ? _.map(array, iterator) : array;
var result = [];
_.reduce(initial, function(memo, el, i) {
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
memo[memo.length] = el;
result[result.length] = array[i];
}
return memo;
}, []);
return result;
};
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = function() {
return _.uniq(_.flatten(arguments, true));
};
// Produce an array that contains every item shared between all the
// passed-in arrays. (Aliased as "intersect" for back-compat.)
_.intersection = _.intersect = function(array) {
var rest = slice.call(arguments, 1);
return _.filter(_.uniq(array), function(item) {
return _.every(rest, function(other) {
return _.indexOf(other, item) >= 0;
});
});
};
// Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain.
_.difference = function(array) {
var rest = _.flatten(slice.call(arguments, 1));
return _.filter(array, function(value){ return !_.include(rest, value); });
};
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = function() {
var args = slice.call(arguments);
var length = _.max(_.pluck(args, 'length'));
var results = new Array(length);
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
return results;
};
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
// we need this function. Return the position of the first occurrence of an
// item in an array, or -1 if the item is not included in the array.
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i, l;
if (isSorted) {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;
return -1;
};
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item) {
if (array == null) return -1;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
var i = array.length;
while (i--) if (i in array && array[i] === item) return i;
return -1;
};
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) {
if (arguments.length <= 1) {
stop = start || 0;
start = 0;
}
step = arguments[2] || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
while(idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
// Function (ahem) Functions
// ------------------
// Reusable constructor function for prototype setting.
var ctor = function(){};
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Binding with arguments is also known as `curry`.
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
// We check for `func.bind` first, to fail fast when `func` is undefined.
_.bind = function bind(func, context) {
var bound, args;
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
};
// Bind all of an object's methods to that object. Useful for ensuring that
// all callbacks defined on an object belong to it.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length == 0) funcs = _.functions(obj);
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = function(func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function(){ return func.apply(func, args); }, wait);
};
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = function(func) {
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
var context, args, timeout, throttling, more;
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
return function() {
context = this; args = arguments;
var later = function() {
timeout = null;
if (more) func.apply(context, args);
whenDone();
};
if (!timeout) timeout = setTimeout(later, wait);
if (throttling) {
more = true;
} else {
func.apply(context, args);
}
whenDone();
throttling = true;
};
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
_.debounce = function(func, wait) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
return memo = func.apply(this, arguments);
};
};
// Returns the first function passed as an argument to the second,
// allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return function() {
var args = [func].concat(slice.call(arguments, 0));
return wrapper.apply(this, args);
};
};
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
var funcs = arguments;
return function() {
var args = arguments;
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
return args[0];
};
};
// Returns a function that will only be executed after being called N times.
_.after = function(times, func) {
if (times <= 0) return func();
return function() {
if (--times < 1) { return func.apply(this, arguments); }
};
};
// Object Functions
// ----------------
// Retrieve the names of an object's properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
return keys;
};
// Retrieve the values of an object's properties.
_.values = function(obj) {
return _.map(obj, _.identity);
};
// Return a sorted list of the function names available on the object.
// Aliased as `methods`
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
obj[prop] = source[prop];
}
});
return obj;
};
// Fill in a given object with default properties.
_.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop];
}
});
return obj;
};
// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
// Invokes interceptor with the obj, and then returns obj.
// The primary purpose of this method is to "tap into" a method chain, in
// order to perform operations on intermediate results within the chain.
_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
// Internal recursive comparison function.
function eq(a, b, stack) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
if (a === b) return a !== 0 || 1 / a == 1 / b;
// A strict comparison is necessary because `null == undefined`.
if (a == null || b == null) return a === b;
// Unwrap any wrapped objects.
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// Invoke a custom `isEqual` method if one is provided.
if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);
if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);
// Compare `[[Class]]` names.
var className = toString.call(a);
if (className != toString.call(b)) return false;
switch (className) {
// Strings, numbers, dates, and booleans are compared by value.
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return a == String(b);
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a == +b;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
a.global == b.global &&
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
}
if (typeof a != 'object' || typeof b != 'object') return false;
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
var length = stack.length;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (stack[length] == a) return true;
}
// Add the first object to the stack of traversed objects.
stack.push(a);
var size = 0, result = true;
// Recursively compare objects and arrays.
if (className == '[object Array]') {
// Compare array lengths to determine if a deep comparison is necessary.
size = a.length;
result = size == b.length;
if (result) {
// Deep compare the contents, ignoring non-numeric properties.
while (size--) {
// Ensure commutative equality for sparse arrays.
if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;
}
}
} else {
// Objects with different constructors are not equivalent.
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
// Deep compare objects.
for (var key in a) {
if (_.has(a, key)) {
// Count the expected number of properties.
size++;
// Deep compare each member.
if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;
}
}
// Ensure that both objects contain the same number of properties.
if (result) {
for (key in b) {
if (_.has(b, key) && !(size--)) break;
}
result = !size;
}
}
// Remove the first object from the stack of traversed objects.
stack.pop();
return result;
}
// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
return eq(a, b, []);
};
// Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) {
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false;
return true;
};
// Is a given value a DOM element?
_.isElement = function(obj) {
return !!(obj && obj.nodeType == 1);
};
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) == '[object Array]';
};
// Is a given variable an object?
_.isObject = function(obj) {
return obj === Object(obj);
};
// Is a given variable an arguments object?
_.isArguments = function(obj) {
return toString.call(obj) == '[object Arguments]';
};
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return !!(obj && _.has(obj, 'callee'));
};
}
// Is a given value a function?
_.isFunction = function(obj) {
return toString.call(obj) == '[object Function]';
};
// Is a given value a string?
_.isString = function(obj) {
return toString.call(obj) == '[object String]';
};
// Is a given value a number?
_.isNumber = function(obj) {
return toString.call(obj) == '[object Number]';
};
// Is the given value `NaN`?
_.isNaN = function(obj) {
// `NaN` is the only value for which `===` is not reflexive.
return obj !== obj;
};
// Is a given value a boolean?
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
};
// Is a given value a date?
_.isDate = function(obj) {
return toString.call(obj) == '[object Date]';
};
// Is the given value a regular expression?
_.isRegExp = function(obj) {
return toString.call(obj) == '[object RegExp]';
};
// Is a given value equal to null?
_.isNull = function(obj) {
return obj === null;
};
// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
// Has own property?
_.has = function(obj, key) {
return hasOwnProperty.call(obj, key);
};
// Utility Functions
// -----------------
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// Keep the identity function around for default iterators.
_.identity = function(value) {
return value;
};
// Run a function **n** times.
_.times = function (n, iterator, context) {
for (var i = 0; i < n; i++) iterator.call(context, i);
};
// Escape a string for HTML interpolation.
_.escape = function(string) {
return (''+string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g,'&#x2F;');
};
// Add your own custom functions to the Underscore object, ensuring that
// they're correctly added to the OOP wrapper as well.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
addToWrapper(name, _[name] = obj[name]);
});
};
// Generate a unique integer id (unique within the entire client session).
// Useful for temporary DOM ids.
var idCounter = 0;
_.uniqueId = function(prefix) {
var id = idCounter++;
return prefix ? prefix + id : id;
};
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /.^/;
// Within an interpolation, evaluation, or escaping, remove HTML escaping
// that had been previously added.
var unescape = function(code) {
return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(str, data) {
var c = _.templateSettings;
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
'with(obj||{}){__p.push(\'' +
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(c.escape || noMatch, function(match, code) {
return "',_.escape(" + unescape(code) + "),'";
})
.replace(c.interpolate || noMatch, function(match, code) {
return "'," + unescape(code) + ",'";
})
.replace(c.evaluate || noMatch, function(match, code) {
return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
})
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
+ "');}return __p.join('');";
var func = new Function('obj', '_', tmpl);
if (data) return func(data, _);
return function(data) {
return func.call(this, data, _);
};
};
// Add a "chain" function, which will delegate to the wrapper.
_.chain = function(obj) {
return _(obj).chain();
};
// The OOP Wrapper
// ---------------
// If Underscore is called as a function, it returns a wrapped object that
// can be used OO-style. This wrapper holds altered versions of all the
// underscore functions. Wrapped objects may be chained.
var wrapper = function(obj) { this._wrapped = obj; };
// Expose `wrapper.prototype` as `_.prototype`
_.prototype = wrapper.prototype;
// Helper function to continue chaining intermediate results.
var result = function(obj, chain) {
return chain ? _(obj).chain() : obj;
};
// A method to easily add functions to the OOP wrapper.
var addToWrapper = function(name, func) {
wrapper.prototype[name] = function() {
var args = slice.call(arguments);
unshift.call(args, this._wrapped);
return result(func.apply(_, args), this._chain);
};
};
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
// Add all mutator Array functions to the wrapper.
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
var wrapped = this._wrapped;
method.apply(wrapped, arguments);
var length = wrapped.length;
if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];
return result(wrapped, this._chain);
};
});
// Add all accessor Array functions to the wrapper.
each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
return result(method.apply(this._wrapped, arguments), this._chain);
};
});
// Start chaining a wrapped Underscore object.
wrapper.prototype.chain = function() {
this._chain = true;
return this;
};
// Extracts the result from a wrapped and chained object.
wrapper.prototype.value = function() {
return this._wrapped;
};
}).call(this);

View File

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://localhost/im_livechat/static/ext/static/lib/requirejs/require.js"></script>
<script type="text/javascript" src='http://localhost/im_livechat/loader?p={"db":"testtrunk","channel":1}'></script>
</head>
<body style="height:100%; margin:0; padding:0;">
<iframe src="http://openerp.com" height="100%" width=100%"></iframe>
</body>
</html>

View File

@ -0,0 +1,146 @@
/*
This file must compile in EcmaScript 3 and work in IE7.
*/
(function() {
"use strict";
var _t = openerp._t;
var im_livechat = {};
openerp.im_livechat = im_livechat;
/*
The state of anonymous session is hold by the client and not the server.
Override the method managing the state of normal conversation.
*/
openerp.im_chat.Conversation.include({
init: function(){
this._super.apply(this, arguments);
this.shown = true;
this.loading_history = false; // unactivate the loading history
},
show: function(){
this._super.apply(this, arguments);
this.shown = true;
},
hide: function(){
this._super.apply(this, arguments);
this.shown = false;
},
update_fold_state: function(state){
if(state === 'closed'){
this.destroy();
}else{
if(state === 'open'){
this.show();
}else{
if(this.shown){
state = 'fold';
this.hide();
}else{
state = 'open';
this.show();
}
}
}
var session = this.get('session');
session.state = state;
this.set('session', session);
},
});
im_livechat.LiveSupport = openerp.Widget.extend({
init: function(server_url, db, channel, options) {
options = options || {};
_.defaults(options, {
buttonText: _t("Chat with one of our collaborators"),
inputPlaceholder: null,
defaultMessage: _t("How may I help you?"),
defaultUsername: _t("Anonymous"),
});
openerp.session = new openerp.Session();
// load the qweb templates
var defs = [];
var templates = ['/im_livechat/static/src/xml/im_livechat.xml','/im_chat/static/src/xml/im_chat.xml'];
_.each(templates, function(tmpl){
defs.push(openerp.session.rpc('/web/proxy/load', {path: tmpl}).then(function(xml) {
openerp.qweb.add_template(xml);
}));
});
return $.when.apply($, defs).then(function() {
return openerp.session.rpc("/im_livechat/available", {db: db, channel: channel}).then(function(activated) {
if(activated){
var button = new im_livechat.ChatButton(null, channel, options);
button.appendTo($("body"));
if (options.auto){
button.click();
}
}
});
});
},
});
im_livechat.ChatButton = openerp.Widget.extend({
className: "openerp_style oe_chat_button",
events: {
"click": "click"
},
init: function(parent, channel, options) {
this._super(parent);
this.channel = channel;
this.options = options;
this.text = options.buttonText;
},
start: function() {
this.$().append(openerp.qweb.render("chatButton", {widget: this}));
},
click: function() {
if (! this.manager) {
this.manager = new openerp.im_chat.ConversationManager(this, this.options);
this.manager.set("bottom_offset", $('.oe_chat_button').outerHeight()); // TODO correct the value (no hardcode damned !)
// override the notification default function
this.manager.notification = function(notif){
}
}
return this.chat();
},
chat: function() {
var self = this;
if (_.keys(this.manager.sessions).length > 0)
return;
openerp.session.rpc("/im_livechat/get_session", {"channel_id" : self.channel, "anonymous_name" : this.options["defaultUsername"]}, {shadow: true}).then(function(session) {
if (! session) {
self.manager.notification(_t("None of our collaborators seems to be available, please try again later."));
return;
}
var conv = self.manager.activate_session(session, [], true);
// start the polling
openerp.bus.bus.add_channel(session.uuid);
openerp.bus.bus.start_polling();
// add the automatic welcome message
if(session.users.length > 0){
if (self.options.defaultMessage) {
setTimeout(function(){
conv.received_message({
id : 1,
type: "message",
message: self.options.defaultMessage,
create_date: openerp.datetime_to_str(new Date()),
from_id: [session.users[0].id, session.users[0].name],
to_id: [0, session.uuid]
});
}, 1000);
}
}
});
}
});
return im_livechat;
})();

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- vim:fdn=3:
-->
<openerp>
<data>
<!-- Template rendering the external HTML support page -->
<template id="support_page" name="Livechat Support Page">
&lt;!DOCTYPE html&gt;
<html style="height: 100%">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title><t t-esc="channel_name"/> Livechat Support Page</title>
<!-- Call the external Bundle to render the css, js, and js loader tags -->
<t t-call="im_livechat.external_loader"/>
<style type="text/css">
body {
height: 100%;
font-size: 16px;
font-weight: 400;
font-family: "Lato", "Lucida Grande", "Helvetica neue", "Helvetica", "Verdana", "Arial", sans-serif;
overflow: hidden;
overflow-y: auto;
display: block;
margin: 0;
padding: 0;
border: none;
width: 100%;
height: 100%;
background: #C9C8E0;
background-image: -webkit-linear-gradient(top, #7c7bad, #ddddee);
background-image: -moz-linear-gradient(top, #7c7bad, #ddddee);
background-image: -ms-linear-gradient(top, #7c7bad, #ddddee);
background-image: -o-linear-gradient(top, #7c7bad, #ddddee);
background-image: linear-gradient(to bottom, #7c7bad, #ddddee);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#7c7bad', endColorstr='#ddddee',GradientType=0 );
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
}
.main {
position: absolute;
opacity: 0;
top: 50%;
width: 100%;
margin-top: -150px;
color: white;
text-shadow: 0 1px 0 rgba(34, 52, 72, 0.2);
text-align: center;
}
.main h1 {
font-size: 54px;
}
.main div {
font-style: italic;
}
</style>
</head>
<body>
<div class="main" style="opacity: 1;">
<h1 class="channel_name"><t t-esc="channel_name"/></h1>
<div>Live Chat Powered by <strong>Odoo</strong>.</div>
</div>
</body>
</html>
</template>
<!-- Template rendering all the scripts required to execute the Livechat from an external page (which not contain Odoo) -->
<template id="external_loader" name="All the scripts to launch the LiveSupport from an external Web Page">
<!-- css style -->
<link t-att-href="'%s/web/css/im_livechat.external_lib' % (url)" rel="stylesheet"/>
<!-- js of all the required lib (internal and external) -->
<script t-att-src="'%s/web/js/im_livechat.external_lib' % (url)" type="text/javascript" />
<!-- the loader -->
<script t-att-src="'%s/im_livechat/loader/%s/%i' % (url, dbname, channel)" type="text/javascript" />
</template>
<!-- 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 t-att-href="'%s/web/css/im_livechat.internal_lib' % (url)" rel="stylesheet"/>
<!-- js of all the required lib (internal and external) -->
<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" />
</template>
<!-- Bundle of External Librairies of the Livechat -->
<template id="external_lib" name="External Librairies of the Livechat, required to make it work">
<!-- OpenERP minimal lib -->
<script type="text/javascript" src="/web/static/lib/underscore/underscore.js"></script>
<script type="text/javascript" src="/web/static/lib/underscore.string/lib/underscore.string.js"></script>
<script type="text/javascript" src="/web/static/lib/jquery/jquery.js"></script>
<script type="text/javascript" src="/web/static/lib/qweb/qweb2.js"></script>
<script type="text/javascript" src="/web/static/src/js/openerpframework.js"></script>
<!-- add the internal lib -->
<t t-call="im_livechat.internal_lib"/>
</template>
<!-- Bundle of Librairies of the Bus, Chat, Livechat -->
<template id="internal_lib" name="Librairies of the Livechat">
<!-- Datejs -->
<script type="text/javascript" src="/web/static/lib/datejs/globalization/en-US.js"></script>
<script type="text/javascript" src="/web/static/lib/datejs/core.js"></script>
<script type="text/javascript" src="/web/static/lib/datejs/parser.js"></script>
<script type="text/javascript" src="/web/static/lib/datejs/sugarpak.js"></script>
<script type="text/javascript" src="/web/static/lib/datejs/extras.js"></script>
<!-- IM module -->
<script type="text/javascript" src="/bus/static/src/js/bus.js"></script>
<script type="text/javascript" src="/im_chat/static/src/js/im_chat.js"></script>
<script type="text/javascript" src="/im_livechat/static/lib/jquery-achtung/src/ui.achtung.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/js/im_livechat.js"></script>
<!-- CSS -->
<link rel="stylesheet" href="/im_chat/static/src/css/im_common.css"></link>
<link rel="stylesheet" href="/im_livechat/static/lib/jquery-achtung/src/ui.achtung.css"></link>
</template>
<!-- the js code to initialize the LiveSupport object -->
<template id="loader" name="Javascript initializing the LiveSupport">
(function() {
window.livesupport = new openerp.im_livechat.LiveSupport(
"<t t-esc="url"/>",
"<t t-esc="dbname"/>",
<t t-esc="channel"/>,
{
buttonText: "<t t-esc="buttonText"/>",
inputPlaceholder: "<t t-esc="inputPlaceholder"/>",
defaultMessage: "<t t-esc="defaultMessage"/>" || '',
auto: window.oe_im_livechat_auto || false,
defaultUsername: "<t t-esc="username"/>" || undefined,
});
})();
</template>
</data>
</openerp>

View File

@ -60,7 +60,7 @@
<field name="name">support_channel.form</field>
<field name="model">im_livechat.channel</field>
<field name="arch" type="xml">
<form string="Support Channels">
<form string="Support Channels" version="7.0">
<sheet>
<field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
<div class="oe_title">
@ -108,11 +108,12 @@
<p>
Copy and paste this code into your website, within the &amp;lt;head&amp;gt; tag:
</p>
<field name="script" readonly="1" class="oe_tag"/>
<field name="script_external" readonly="1" class="oe_tag"/>
<p>
or copy this url and send it by email to your customers or suppliers:
</p>
<field name="web_page" readonly="1" class="oe_tag"/>
<p>For website built with Odoo CMS, please install the website_livechat module. Then go to Settings > Website Settings and select the Live Chat Channel you want to add on your website.</p>
</div>
</sheet>
@ -122,20 +123,20 @@
<record model="ir.actions.act_window" id="action_history">
<field name="name">History</field>
<field name="res_model">im.message</field>
<field name="res_model">im_chat.message</field>
<field name="view_mode">list</field>
<field name="domain">[('session_id.channel_id', '!=', None)]</field>
<field name="context">{'search_default_group_by_session_id': 1, 'search_default_group_by_date': 1, 'search_default_session_id': 1}</field>
<field name="domain">[('to_id.channel_id', '!=', None)]</field>
<field name="context">{'search_default_group_by_to_id': 1}</field>
</record>
<menuitem name="History" parent="im_livechat" id="history" action="action_history" groups="group_im_livechat_manager"/>
<record id="im_message_form" model="ir.ui.view">
<field name="name">im.message.tree</field>
<field name="model">im.message</field>
<field name="name">im.chat.message.tree</field>
<field name="model">im_chat.message</field>
<field name="arch" type="xml">
<tree string="History" create="false">
<field name="session_id"/>
<field name="date"/>
<field name="to_id"/>
<field name="create_date"/>
<field name="from_id"/>
<field name="message"/>
</tree>
@ -143,16 +144,16 @@
</record>
<record id="im_message_search" model="ir.ui.view">
<field name="name">im.message.search</field>
<field name="model">im.message</field>
<field name="name">im.chat.message.search</field>
<field name="model">im_chat.message</field>
<field name="arch" type="xml">
<search string="Search history">
<filter name="session_id" string="My Sessions" domain="[('session_id.user_ids','in', uid)]"/>
<filter name="to_id" string="My Sessions" domain="[('to_id.user_ids','in', uid)]"/>
<field name="from_id"/>
<field name="to_id"/>
<group expand="0" string="Group By">
<filter name="group_by_session_id" string="Session" domain="[]" context="{'group_by':'session_id'}"/>
<filter name="group_by_date" string="Date" domain="[]" context="{'group_by':'date'}"/>
<group expand="0" string="Group By...">
<filter name="group_by_to_id" string="Session" domain="[]" context="{'group_by':'to_id'}"/>
<filter name="group_by_date" string="Date" domain="[]" context="{'group_by':'create_date'}"/>
</group>
</search>
</field>

View File

@ -1,61 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
window.oe_im_livechat_auto = true;
</script>
{{script}}
<style type="text/css">
body {
height: 100%;
font-size: 16px;
font-weight: 400;
font-family: "Lato", "Lucida Grande", "Helvetica neue", "Helvetica", "Verdana", "Arial", sans-serif;
overflow: hidden;
overflow-y: auto;
display: block;
margin: 0;
padding: 0;
border: none;
width: 100%;
height: 100%;
background: #C9C8E0;
background-image: -webkit-linear-gradient(top, #7c7bad, #ddddee);
background-image: -moz-linear-gradient(top, #7c7bad, #ddddee);
background-image: -ms-linear-gradient(top, #7c7bad, #ddddee);
background-image: -o-linear-gradient(top, #7c7bad, #ddddee);
background-image: linear-gradient(to bottom, #7c7bad, #ddddee);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#7c7bad', endColorstr='#ddddee',GradientType=0 );
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
}
.main {
position: absolute;
opacity: 0;
top: 50%;
width: 100%;
margin-top: -150px;
color: white;
text-shadow: 0 1px 0 rgba(34, 52, 72, 0.2);
z-index: 10;
text-align: center;
}
.main h1 {
font-size: 54px;
}
.main div {
font-style: italic;
}
</style>
</head>
<body>
<div class="main" style="opacity: 1;">
<h1 class="channel_name">{{channelName | escape}}</h1>
<div>Live Chat Powered by <strong>OpenERP</strong>.</div>
</div>
</body>
</html>

View File

@ -1,12 +1,12 @@
{
'name': 'LiveChat',
'name': 'Website Live Support',
'category': 'Website',
'summary': 'Chat With Your Website Visitors',
'version': '1.0',
'description': """
OpenERP Website LiveChat
Odoo Website LiveChat
========================
For website built with Odoo CMS, this module include a chat button on your Website, and allow your visitors to chat with your collabarators.
""",
'author': 'OpenERP SA',
'depends': ['website', 'im_livechat'],

View File

@ -2,10 +2,10 @@
<openerp>
<data>
<template id="header" inherit_id="website.layout" name="LiveChat Snippet">
<xpath expr="//body" position="inside">
<template id="header" inherit_id="website.layout" name="LiveChat Import (internal) Scripts">
<xpath expr="//head" position="inside">
<t t-if="website.channel_id">
<t t-raw="website.channel_id.script"/>
<t t-raw="website.channel_id.script_internal"/>
</t>
</xpath>
</template>