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
|
@ -0,0 +1 @@
|
|||
import bus
|
|
@ -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',
|
||||
],
|
||||
}
|
|
@ -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:
|
|
@ -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
|
|
|
@ -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;
|
||||
})();
|
|
@ -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>
|
|
@ -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']);
|
||||
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
|
||||
import im
|
362
addons/im/im.py
|
@ -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"),
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"grunt": "*",
|
||||
"grunt-contrib-jshint": "*"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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,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();
|
||||
};
|
||||
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
|
||||
})();
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
import im_chat
|
|
@ -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,
|
||||
}
|
|
@ -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:
|
|
@ -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>
|
|
@ -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
|
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
@ -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;
|
|
@ -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;
|
||||
}
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 74 B After Width: | Height: | Size: 74 B |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 830 B After Width: | Height: | Size: 830 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 1017 B |
After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
After Width: | Height: | Size: 855 B |
|
@ -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 = {
|
||||
":'(": "😢",
|
||||
":O" : "😱",
|
||||
"3:)": "😈",
|
||||
":)" : "😊",
|
||||
":D" : "😅",
|
||||
";)" : "😉",
|
||||
":p" : "😋",
|
||||
":(" : "☹",
|
||||
":|" : "😐",
|
||||
":/" : "😏",
|
||||
"8)" : "😳",
|
||||
":s" : "😖",
|
||||
":pinky" : "<img src='/im_chat/static/src/img/pinky.png'/>",
|
||||
":musti" : "<img src='/im_chat/static/src/img/musti.png'/>",
|
||||
};
|
||||
if(kitten){
|
||||
_.extend(smileys, {
|
||||
":)" : "😺",
|
||||
":D" : "😹",
|
||||
";)" : "😼",
|
||||
":p" : "😽",
|
||||
":(" : "🙀",
|
||||
":|" : "😿",
|
||||
});
|
||||
}
|
||||
return smileys;
|
||||
},
|
||||
smiley: function(str){
|
||||
var re_escape = function(str){
|
||||
return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
|
||||
};
|
||||
var smileys = this.get_smiley_list();
|
||||
_.each(_.keys(smileys), function(key){
|
||||
str = str.replace( new RegExp("(?:^|\\s)(" + re_escape(key) + ")(?:\\s|$)"), ' <span class="smiley">'+smileys[key]+'</span> ');
|
||||
});
|
||||
return str;
|
||||
},
|
||||
escape_keep_url: function(str){
|
||||
var url_regex = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi;
|
||||
var last = 0;
|
||||
var txt = "";
|
||||
while (true) {
|
||||
var result = url_regex.exec(str);
|
||||
if (! result)
|
||||
break;
|
||||
txt += _.escape(str.slice(last, result.index));
|
||||
last = url_regex.lastIndex;
|
||||
var url = _.escape(result[0]);
|
||||
txt += '<a href="' + url + '" target="_blank">' + url + '</a>';
|
||||
}
|
||||
txt += _.escape(str.slice(last, str.length));
|
||||
return txt;
|
||||
},
|
||||
_go_bottom: function() {
|
||||
this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight);
|
||||
},
|
||||
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;
|
||||
})();
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,2 +1 @@
|
|||
|
||||
import im_livechat
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
|
@ -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"/>
|
||||
|
|
|
@ -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,3 +0,0 @@
|
|||
{
|
||||
"directory": "static/lib/"
|
||||
}
|
|
@ -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']);
|
||||
|
||||
};
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"grunt": "*",
|
||||
"grunt-contrib-jshint": "*"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
|
@ -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; }
|
|
@ -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);
|
|
@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
if (attribute) {
|
||||
s = s.replace(/"/g, '"');
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
|
@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
|
||||
};
|
||||
|
||||
// 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);
|
|
@ -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>
|
|
@ -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;
|
||||
|
||||
})();
|
|
@ -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">
|
||||
<!DOCTYPE html>
|
||||
<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>
|
|
@ -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 &lt;head&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>
|
|
@ -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>
|
|
@ -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'],
|
||||
|
|
|
@ -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>
|
||||
|
|