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',
|
'name' : 'Instant Messaging',
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'summary': 'Live Chat, Talks with Others',
|
'summary': 'OpenERP Chat',
|
||||||
|
'author': 'OpenERP SA',
|
||||||
'sequence': '18',
|
'sequence': '18',
|
||||||
'category': 'Tools',
|
'category': 'Tools',
|
||||||
'complexity': 'easy',
|
'complexity': 'easy',
|
||||||
|
@ -16,11 +17,9 @@ chat in real time. It support several chats in parallel.
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'security/im_security.xml',
|
'security/im_security.xml',
|
||||||
'views/im.xml',
|
'views/im_chat.xml',
|
||||||
],
|
],
|
||||||
'depends' : ['base', 'web'],
|
'depends' : ['base', 'web', 'bus'],
|
||||||
'qweb': ['static/src/xml/*.xml'],
|
'qweb': ['static/src/xml/*.xml'],
|
||||||
'installable': True,
|
|
||||||
'auto_install': False,
|
|
||||||
'application': True,
|
'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 {
|
.oe_im_frame_header .oe_im_search_icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: #888;
|
color: #888;
|
||||||
top: 2px;
|
top: -4px;
|
||||||
left: 9px;
|
left: 9px;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-family: "entypoRegular" !important;
|
font-family: "entypoRegular" !important;
|
||||||
|
@ -106,4 +106,4 @@
|
||||||
height: 11px;
|
height: 11px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
|
@ -73,19 +73,36 @@
|
||||||
border-bottom: 1px solid #AEB9BD;
|
border-bottom: 1px solid #AEB9BD;
|
||||||
cursor: pointer;
|
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;
|
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;
|
cursor: pointer;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
margin-left: 5px;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
float: right;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: black;
|
|
||||||
text-shadow: 0 1px 0 white;
|
text-shadow: 0 1px 0 white;
|
||||||
opacity: 0.2;
|
opacity: 0.9;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.oe_im_chatview .oe_im_chatview_content {
|
.oe_im_chatview .oe_im_chatview_content {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -120,15 +137,34 @@
|
||||||
-webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
|
-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);
|
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;
|
background: white;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 32px;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
-moz-border-radius: 3px;
|
-moz-border-radius: 3px;
|
||||||
-webkit-border-radius: 3px;
|
-webkit-border-radius: 3px;
|
||||||
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 {
|
.oe_im_chatview .oe_im_chatview_clip {
|
||||||
position: relative;
|
position: relative;
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -187,3 +223,13 @@
|
||||||
width: 11px;
|
width: 11px;
|
||||||
height: 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
|
import im_livechat
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
'name' : 'Live Support',
|
'name' : 'Live Support',
|
||||||
|
'author': 'OpenERP SA',
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'summary': 'Live Chat with Visitors/Customers',
|
'summary': 'Live Chat with Visitors/Customers',
|
||||||
'category': 'Tools',
|
'category': 'Tools',
|
||||||
|
@ -17,12 +18,13 @@ chat operators.
|
||||||
'data': [
|
'data': [
|
||||||
"security/im_livechat_security.xml",
|
"security/im_livechat_security.xml",
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
"im_livechat_view.xml",
|
"views/im_livechat_view.xml",
|
||||||
|
"views/im_livechat.xml"
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
"im_livechat_demo.xml",
|
"im_livechat_demo.xml",
|
||||||
],
|
],
|
||||||
'depends' : ["im", "mail"],
|
'depends' : ["mail", "im_chat"],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'auto_install': False,
|
'auto_install': False,
|
||||||
'application': True,
|
'application': True,
|
||||||
|
|
|
@ -19,66 +19,16 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
import json
|
|
||||||
import random
|
import random
|
||||||
import jinja2
|
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
import openerp.addons.im.im as im
|
import openerp.addons.im_chat.im_chat
|
||||||
|
|
||||||
from openerp.osv import osv, fields
|
from openerp.osv import osv, fields
|
||||||
from openerp import tools
|
from openerp import tools
|
||||||
from openerp import http
|
from openerp import http
|
||||||
from openerp.http import request
|
from openerp.http import request
|
||||||
|
|
||||||
env = jinja2.Environment(
|
class im_livechat_channel(osv.Model):
|
||||||
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):
|
|
||||||
_name = 'im_livechat.channel'
|
_name = 'im_livechat.channel'
|
||||||
|
|
||||||
def _get_default_image(self, cr, uid, context=None):
|
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):
|
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)
|
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):
|
def _are_you_inside(self, cr, uid, ids, name, arg, context=None):
|
||||||
res = {}
|
res = {}
|
||||||
for record in self.browse(cr, uid, ids, context=context):
|
for record in self.browse(cr, uid, ids, context=context):
|
||||||
|
@ -103,31 +52,45 @@ class im_livechat_channel(osv.osv):
|
||||||
break
|
break
|
||||||
return res
|
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 = {}
|
res = {}
|
||||||
for record in self.browse(cr, uid, ids, context=context):
|
for record in self.browse(cr, uid, ids, context=context):
|
||||||
res[record.id] = env.get_template("include.html").render({
|
values["channel"] = record.id
|
||||||
"url": self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url'),
|
res[record.id] = self.pool['ir.ui.view'].render(cr, uid, 'im_livechat.external_loader', values, context=context)
|
||||||
"parameters": {"db":cr.dbname, "channel":record.id},
|
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
|
return res
|
||||||
|
|
||||||
def _web_page(self, cr, uid, ids, name, arg, context=None):
|
def _web_page(self, cr, uid, ids, name, arg, context=None):
|
||||||
res = {}
|
res = {}
|
||||||
for record in self.browse(cr, uid, ids, context=context):
|
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') + \
|
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
|
return res
|
||||||
|
|
||||||
_columns = {
|
_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"),
|
'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),
|
'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"),
|
'web_page': fields.function(_web_page, type='url', string='Web Page', store=False, size="200"),
|
||||||
'button_text': fields.char(string="Text of the Button"),
|
'button_text': fields.char(string="Text of the Button", size=200),
|
||||||
'input_placeholder': fields.char(string="Chat Input Placeholder"),
|
'input_placeholder': fields.char(string="Chat Input Placeholder", size=200),
|
||||||
'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."),
|
'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: all image fields are base64 encoded and PIL-supported
|
||||||
'image': fields.binary("Photo",
|
'image': fields.binary("Photo",
|
||||||
help="This field holds the image used as photo for the group, limited to 1024x1024px."),
|
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):
|
def get_available_users(self, cr, uid, channel_id, context=None):
|
||||||
channel = self.browse(cr, openerp.SUPERUSER_ID, channel_id, context=context)
|
""" get available user of a given channel """
|
||||||
im_user_ids = self.pool.get("im.user").search(cr, uid, [["user_id", "in", [user.id for user in channel.user_ids]]], context=context)
|
channel = self.browse(cr, uid, channel_id, context=context)
|
||||||
users = []
|
users = []
|
||||||
for iuid in im_user_ids:
|
for user_id in channel.user_ids:
|
||||||
imuser = self.pool.get("im.user").browse(cr, uid, iuid, context=context)
|
if (user_id.im_status == 'online'):
|
||||||
if imuser.im_status:
|
users.append(user_id)
|
||||||
users.append(imuser)
|
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def get_session(self, cr, uid, channel_id, uuid, context=None):
|
def get_channel_session(self, cr, uid, channel_id, anonymous_name, context=None):
|
||||||
self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
|
""" return a session given a channel : create on with a registered user, or return false otherwise """
|
||||||
users = self.get_available_users(cr, openerp.SUPERUSER_ID, channel_id, context=context)
|
# get the avalable user of the channel
|
||||||
|
users = self.get_available_users(cr, uid, channel_id, context=context)
|
||||||
if len(users) == 0:
|
if len(users) == 0:
|
||||||
return False
|
return False
|
||||||
user_id = random.choice(users).id
|
user_id = random.choice(users).id
|
||||||
session = self.pool.get("im.session").session_get(cr, uid, [user_id], uuid, context=context)
|
# create the session, and add the link with the given channel
|
||||||
self.pool.get("im.session").write(cr, openerp.SUPERUSER_ID, session.get("id"), {'channel_id': channel_id}, context=context)
|
Session = self.pool["im_chat.session"]
|
||||||
return session.get("id")
|
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):
|
def test_channel(self, cr, uid, channel, context=None):
|
||||||
if not channel:
|
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):
|
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)
|
chan = self.browse(cr, uid, channel, context=context)
|
||||||
return {
|
return {
|
||||||
"url": url,
|
"url": url,
|
||||||
|
@ -207,9 +171,69 @@ class im_livechat_channel(osv.osv):
|
||||||
self.write(cr, uid, ids, {'user_ids': [(3, uid)]})
|
self.write(cr, uid, ids, {'user_ids': [(3, uid)]})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class im_session(osv.osv):
|
class im_chat_session(osv.Model):
|
||||||
_inherit = 'im.session'
|
_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 = {
|
_columns = {
|
||||||
|
'anonymous_name' : fields.char('Anonymous Name'),
|
||||||
'channel_id': fields.many2one("im_livechat.channel", "Channel"),
|
'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">
|
<record id="message_rule_1" model="ir.rule">
|
||||||
<field name="name">Live Support Managers can read messages from live support</field>
|
<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="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_unlink" eval="0"/>
|
||||||
<field name="perm_write" eval="0"/>
|
<field name="perm_write" eval="0"/>
|
||||||
<field name="perm_read" eval="1"/>
|
<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_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_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_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="name">support_channel.form</field>
|
||||||
<field name="model">im_livechat.channel</field>
|
<field name="model">im_livechat.channel</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Support Channels">
|
<form string="Support Channels" version="7.0">
|
||||||
<sheet>
|
<sheet>
|
||||||
<field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
|
<field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
|
@ -108,11 +108,12 @@
|
||||||
<p>
|
<p>
|
||||||
Copy and paste this code into your website, within the &lt;head&gt; tag:
|
Copy and paste this code into your website, within the &lt;head&gt; tag:
|
||||||
</p>
|
</p>
|
||||||
<field name="script" readonly="1" class="oe_tag"/>
|
<field name="script_external" readonly="1" class="oe_tag"/>
|
||||||
<p>
|
<p>
|
||||||
or copy this url and send it by email to your customers or suppliers:
|
or copy this url and send it by email to your customers or suppliers:
|
||||||
</p>
|
</p>
|
||||||
<field name="web_page" readonly="1" class="oe_tag"/>
|
<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>
|
</div>
|
||||||
|
|
||||||
</sheet>
|
</sheet>
|
||||||
|
@ -122,20 +123,20 @@
|
||||||
|
|
||||||
<record model="ir.actions.act_window" id="action_history">
|
<record model="ir.actions.act_window" id="action_history">
|
||||||
<field name="name">History</field>
|
<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="view_mode">list</field>
|
||||||
<field name="domain">[('session_id.channel_id', '!=', None)]</field>
|
<field name="domain">[('to_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="context">{'search_default_group_by_to_id': 1}</field>
|
||||||
</record>
|
</record>
|
||||||
<menuitem name="History" parent="im_livechat" id="history" action="action_history" groups="group_im_livechat_manager"/>
|
<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">
|
<record id="im_message_form" model="ir.ui.view">
|
||||||
<field name="name">im.message.tree</field>
|
<field name="name">im.chat.message.tree</field>
|
||||||
<field name="model">im.message</field>
|
<field name="model">im_chat.message</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree string="History" create="false">
|
<tree string="History" create="false">
|
||||||
<field name="session_id"/>
|
<field name="to_id"/>
|
||||||
<field name="date"/>
|
<field name="create_date"/>
|
||||||
<field name="from_id"/>
|
<field name="from_id"/>
|
||||||
<field name="message"/>
|
<field name="message"/>
|
||||||
</tree>
|
</tree>
|
||||||
|
@ -143,16 +144,16 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="im_message_search" model="ir.ui.view">
|
<record id="im_message_search" model="ir.ui.view">
|
||||||
<field name="name">im.message.search</field>
|
<field name="name">im.chat.message.search</field>
|
||||||
<field name="model">im.message</field>
|
<field name="model">im_chat.message</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<search string="Search history">
|
<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="from_id"/>
|
||||||
<field name="to_id"/>
|
<field name="to_id"/>
|
||||||
<group expand="0" string="Group By">
|
<group expand="0" string="Group By...">
|
||||||
<filter name="group_by_session_id" string="Session" domain="[]" context="{'group_by':'session_id'}"/>
|
<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':'date'}"/>
|
<filter name="group_by_date" string="Date" domain="[]" context="{'group_by':'create_date'}"/>
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</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',
|
'category': 'Website',
|
||||||
'summary': 'Chat With Your Website Visitors',
|
'summary': 'Chat With Your Website Visitors',
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'description': """
|
'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',
|
'author': 'OpenERP SA',
|
||||||
'depends': ['website', 'im_livechat'],
|
'depends': ['website', 'im_livechat'],
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
<openerp>
|
<openerp>
|
||||||
<data>
|
<data>
|
||||||
|
|
||||||
<template id="header" inherit_id="website.layout" name="LiveChat Snippet">
|
<template id="header" inherit_id="website.layout" name="LiveChat Import (internal) Scripts">
|
||||||
<xpath expr="//body" position="inside">
|
<xpath expr="//head" position="inside">
|
||||||
<t t-if="website.channel_id">
|
<t t-if="website.channel_id">
|
||||||
<t t-raw="website.channel_id.script"/>
|
<t t-raw="website.channel_id.script_internal"/>
|
||||||
</t>
|
</t>
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|