merge live support branch
bzr revid: nicolas.vanhoren@openerp.com-20130129134439-9rca0b38f2eqd39q
|
@ -0,0 +1,2 @@
|
|||
|
||||
import live_support
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
'name' : 'Live Support',
|
||||
'version': '1.0',
|
||||
'category': 'Tools',
|
||||
'complexity': 'easy',
|
||||
'description':
|
||||
"""
|
||||
OpenERP Live Support
|
||||
====================
|
||||
Allow to drop instant messaging widgets on any web page that will communicate with the current
|
||||
server.
|
||||
""",
|
||||
'data': [
|
||||
],
|
||||
'depends' : [],
|
||||
'js': ['static/src/js/*.js'],
|
||||
'css': ['static/src/css/*.css'],
|
||||
'qweb': ['static/src/xml/*.xml'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
##############################################################################
|
|
@ -0,0 +1 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
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
|
|
@ -0,0 +1,167 @@
|
|||
|
||||
|
||||
|
||||
.openerp_style { /* base style of openerp */
|
||||
font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
|
||||
color: #4c4c4c;
|
||||
font-size: 13px;
|
||||
background: white;
|
||||
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* conversations */
|
||||
|
||||
.oe_im_chatview {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
bottom: 6px;
|
||||
margin-right: 6px;
|
||||
background: rgba(60, 60, 60, 0.8);
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-moz-box-shadow: 0 0 3px rgba(0,0,0,0.3), 0 2px 4px rgba(0,0,0,0.3);
|
||||
-webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
width: 240px;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_disconnected {
|
||||
display:none;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
background: #E8EBEF;
|
||||
padding: 5px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
line-height: 14px;
|
||||
height: 28px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected {
|
||||
display: block;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_header {
|
||||
padding: 3px 6px 2px;
|
||||
background: #DEDEDE;
|
||||
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
|
||||
-moz-border-radius: 3px 3px 0 0;
|
||||
-webkit-border-radius: 3px 3px 0 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom: 1px solid #AEB9BD;
|
||||
cursor: pointer;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_close {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
font-size: 18px;
|
||||
line-height: 16px;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
text-shadow: 0 1px 0 white;
|
||||
opacity: 0.2;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_content {
|
||||
overflow: auto;
|
||||
height: 287px;
|
||||
width: 240px;
|
||||
}
|
||||
.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content {
|
||||
height: 249px;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_footer {
|
||||
position: relative;
|
||||
padding: 3px;
|
||||
border-top: 1px solid #AEB9BD;
|
||||
background: #DEDEDE;
|
||||
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
|
||||
-moz-border-radius: 0 0 3px 3px;
|
||||
-webkit-border-radius: 0 0 3px 3px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_input {
|
||||
width: 222px;
|
||||
font-family: Lato, Helvetica, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
padding: 1px 5px;
|
||||
border: 1px solid #AEB9BD;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-moz-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);
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_bubble {
|
||||
background: white;
|
||||
position: relative;
|
||||
padding: 3px;
|
||||
margin: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_clip {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-right: 4px;
|
||||
-moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25);
|
||||
-webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_avatar {
|
||||
float: left;
|
||||
width: 26px;
|
||||
height: auto;
|
||||
clip: rect(0, 26px, 26px, 0);
|
||||
max-width: 100%;
|
||||
width: auto 9;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
border: 0;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_time {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
margin: 3px;
|
||||
text-align: right;
|
||||
line-height: 13px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
width: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_from {
|
||||
margin: 0 0 2px 0;
|
||||
line-height: 14px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
width: 140px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #3A87AD;
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_bubble_list {
|
||||
}
|
||||
.oe_im_chatview .oe_im_chatview_bubble_item {
|
||||
margin: 0 0 2px 30px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.oe_im_chatview_online {
|
||||
display: none;
|
||||
margin-top: -4px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
}
|
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 74 B |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 830 B |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 100 KiB |
|
@ -0,0 +1,320 @@
|
|||
|
||||
define(["nova", "jquery", "underscore", "oeclient", "require"], function(nova, $, _, oeclient, require) {
|
||||
var livesupport = {};
|
||||
|
||||
var templateEngine = new nova.TemplateEngine();
|
||||
templateEngine.extendEnvironment({"toUrl": _.bind(require.toUrl, require)});
|
||||
var connection;
|
||||
|
||||
livesupport.main = function(server_url, db, login, password) {
|
||||
var templates_def = $.ajax({
|
||||
url: require.toUrl("./livesupport_templates.js"),
|
||||
jsonp: false,
|
||||
jsonpCallback: "oe_livesupport_templates_callback",
|
||||
dataType: "jsonp",
|
||||
cache: true,
|
||||
}).then(function(content) {
|
||||
return templateEngine.loadFileContent(content);
|
||||
});
|
||||
var css_def = $.Deferred();
|
||||
$('<link rel="stylesheet" href="' + require.toUrl("../css/livesupport.css") + '"></link>')
|
||||
.appendTo($("head")).ready(function() {
|
||||
css_def.resolve();
|
||||
});
|
||||
|
||||
$.when(templates_def, css_def).then(function() {
|
||||
console.log("starting client");
|
||||
connection = new oeclient.Connection(new oeclient.JsonpRPCConnector(server_url), db, login, password);
|
||||
connection.getModel("res.users").search_read([["login", "=", ["demo"]]]).then(function(result) {
|
||||
demo_id = result[0].id;
|
||||
var manager = new livesupport.ConversationManager(null);
|
||||
manager.start_polling().then(function() {
|
||||
manager.ensure_users([demo_id]).then(function() {
|
||||
manager.activate_user(manager.get_user(demo_id));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var ERROR_DELAY = 5000;
|
||||
|
||||
livesupport.ImUser = nova.Class.$extend({
|
||||
__include__: [nova.DynamicProperties],
|
||||
__init__: function(parent, user_rec) {
|
||||
nova.DynamicProperties.__init__.call(this, parent);
|
||||
//user_rec.image_url = instance.session.url('/web/binary/image', {model:'res.users', field: 'image_small', 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");
|
||||
nova.DynamicProperties.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);
|
||||
},
|
||||
});
|
||||
|
||||
livesupport.ConversationManager = nova.Class.$extend({
|
||||
__include__: [nova.DynamicProperties],
|
||||
__init__: function(parent) {
|
||||
nova.DynamicProperties.__init__.call(this, parent);
|
||||
this.set("right_offset", 0);
|
||||
this.conversations = [];
|
||||
this.users = {};
|
||||
this.on("change:right_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;
|
||||
},
|
||||
start_polling: function() {
|
||||
var self = this;
|
||||
return this.ensure_users([connection.userId]).then(function() {
|
||||
var me = self.users_cache[connection.userId];
|
||||
delete self.users_cache[connection.userId];
|
||||
self.me = me;
|
||||
connection.connector.call("/longpolling/im/activated", {}).then(function(activated) {
|
||||
if (activated) {
|
||||
self.activated = true;
|
||||
self.poll();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
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;
|
||||
if (_.size(no_cache) === 0)
|
||||
return $.when();
|
||||
else
|
||||
return connection.getModel("im.user").call("read_users", [_.values(no_cache), ["name"]]).then(function(users) {
|
||||
self.add_to_user_cache(users);
|
||||
});
|
||||
},
|
||||
add_to_user_cache: function(user_recs) {
|
||||
_.each(user_recs, function(user_rec) {
|
||||
if (! this.users_cache[user_rec.id]) {
|
||||
var user = new livesupport.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");
|
||||
});
|
||||
connection.connector.call("/longpolling/im/poll", {
|
||||
last: this.last,
|
||||
users_watch: user_ids,
|
||||
db: connection.database,
|
||||
uid: connection.userId,
|
||||
password: connection.password,
|
||||
}).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;
|
||||
var user_ids = _.pluck(_.pluck(result.res, "from"), 0);
|
||||
self.ensure_users(user_ids).then(function() {
|
||||
_.each(result.res, function(mes) {
|
||||
var user = self.get_user(mes.from[0]);
|
||||
self.received_message(mes, user);
|
||||
});
|
||||
self.poll();
|
||||
});
|
||||
}, function() {
|
||||
setTimeout(_.bind(self.poll, self), ERROR_DELAY);
|
||||
});
|
||||
},
|
||||
get_activated: function() {
|
||||
return this.activated;
|
||||
},
|
||||
create_ting: function() {
|
||||
this.ting = new Audio(new Audio().canPlayType("audio/ogg; codecs=vorbis") ?
|
||||
require.toUrl("../audio/Ting.ogg") :
|
||||
require.toUrl("../audio/Ting.mp3")
|
||||
);
|
||||
},
|
||||
window_focus_change: function() {
|
||||
if (this.get("window_focus")) {
|
||||
this.set("waiting_messages", 0);
|
||||
}
|
||||
},
|
||||
messages_change: function() {
|
||||
/*if (! instance.webclient.set_title_part)
|
||||
return;
|
||||
instance.webclient.set_title_part("im_messages", this.get("waiting_messages") === 0 ? undefined :
|
||||
_.str.sprintf(_t("%d Messages"), this.get("waiting_messages")));*/
|
||||
},
|
||||
activate_user: function(user) {
|
||||
if (this.users[user.get('id')]) {
|
||||
return this.users[user.get('id')];
|
||||
}
|
||||
var conv = new livesupport.Conversation(this, user, this.me);
|
||||
conv.appendTo($("body"));
|
||||
conv.on("destroyed", this, function() {
|
||||
this.conversations = _.without(this.conversations, conv);
|
||||
delete this.users[conv.user.get('id')];
|
||||
this.calc_positions();
|
||||
});
|
||||
this.conversations.push(conv);
|
||||
this.users[user.get('id')] = conv;
|
||||
this.calc_positions();
|
||||
return conv;
|
||||
},
|
||||
received_message: function(message, user) {
|
||||
if (! this.get("window_focus")) {
|
||||
this.set("waiting_messages", this.get("waiting_messages") + 1);
|
||||
this.ting.play();
|
||||
this.create_ting();
|
||||
}
|
||||
var conv = this.activate_user(user);
|
||||
conv.received_message(message);
|
||||
},
|
||||
calc_positions: function() {
|
||||
var current = this.get("right_offset");
|
||||
_.each(_.range(this.conversations.length), function(i) {
|
||||
this.conversations[i].set("right_position", current);
|
||||
current += this.conversations[i].$().outerWidth(true);
|
||||
}, this);
|
||||
},
|
||||
destroy: function() {
|
||||
$(window).unbind("blur", this.blur_hdl);
|
||||
$(window).unbind("focus", this.focus_hdl);
|
||||
nova.DynamicProperties.destroy.call(this);
|
||||
},
|
||||
});
|
||||
|
||||
livesupport.Conversation = nova.Widget.$extend({
|
||||
className: "openerp_style oe_im_chatview",
|
||||
events: {
|
||||
"keydown input": "send_message",
|
||||
"click .oe_im_chatview_close": "destroy",
|
||||
"click .oe_im_chatview_header": "show_hide",
|
||||
},
|
||||
__init__: function(parent, user, me) {
|
||||
this.$super(parent);
|
||||
this.me = me;
|
||||
this.user = user;
|
||||
this.user.add_watcher();
|
||||
this.set("right_position", 0);
|
||||
this.shown = true;
|
||||
},
|
||||
render: function() {
|
||||
this.$().append(templateEngine.conversation({widget: this}));
|
||||
var change_status = function() {
|
||||
this.$().toggleClass("oe_im_chatview_disconnected_status", this.user.get("im_status") === false);
|
||||
this.$(".oe_im_chatview_online").toggle(this.user.get("im_status") === true);
|
||||
this._go_bottom();
|
||||
};
|
||||
this.user.on("change:im_status", this, change_status);
|
||||
change_status.call(this);
|
||||
|
||||
this.on("change:right_position", this, this.calc_pos);
|
||||
this.full_height = this.$().height();
|
||||
this.calc_pos();
|
||||
},
|
||||
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;
|
||||
},
|
||||
calc_pos: function() {
|
||||
this.$().css("right", this.get("right_position"));
|
||||
},
|
||||
received_message: function(message) {
|
||||
this._add_bubble(this.user, message.message, oeclient.str_to_datetime(message.date));
|
||||
},
|
||||
send_message: function(e) {
|
||||
if(e && e.which !== 13) {
|
||||
return;
|
||||
}
|
||||
var mes = this.$("input").val();
|
||||
this.$("input").val("");
|
||||
var send_it = _.bind(function() {
|
||||
var model = connection.getModel("im.message");
|
||||
return model.call("post", [mes, this.user.get('id')], {context: {}});
|
||||
}, this);
|
||||
var tries = 0;
|
||||
send_it().then(_.bind(function() {
|
||||
this._add_bubble(this.me, mes, new Date());
|
||||
}, this), function(error, e) {
|
||||
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);
|
||||
|
||||
this.last_bubble = $(templateEngine.conversation_bubble({"items": items, "user": user, "time": date}));
|
||||
$(this.$(".oe_im_chatview_content").children()[0]).append(this.last_bubble);
|
||||
this._go_bottom();
|
||||
},
|
||||
_go_bottom: function() {
|
||||
this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height());
|
||||
},
|
||||
destroy: function() {
|
||||
this.user.remove_watcher();
|
||||
this.trigger("destroyed");
|
||||
return this._super();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
return livesupport;
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
<%def name="conversation">
|
||||
<div class="oe_im_chatview_header">
|
||||
<img src="${toUrl('../img/green.png')}" class="oe_im_chatview_online"/>
|
||||
${widget.user.get('name')}
|
||||
<button class="oe_im_chatview_close">×</button>
|
||||
</div>
|
||||
<div class="oe_im_chatview_disconnected">
|
||||
${widget.user.get("name") + " is offline. He/She will receive your messages on his/her next connection."}
|
||||
</div>
|
||||
<div class="oe_im_chatview_content">
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="oe_im_chatview_footer">
|
||||
<input class="oe_im_chatview_input" placeholder="Say something..." />
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="conversation_bubble">
|
||||
<div class="oe_im_chatview_bubble">
|
||||
<div class="oe_im_chatview_clip">
|
||||
<img class="oe_im_chatview_avatar" src="${user.get('image_url')}"/>
|
||||
</div>
|
||||
<div class="oe_im_chatview_from">${user.get('name')}</div>
|
||||
<div class="oe_im_chatview_bubble_list">
|
||||
% _.each(items, function(item) {
|
||||
<div class="oe_im_chatview_bubble_item">${item}</div>
|
||||
% });
|
||||
</div>
|
||||
<div class="oe_im_chatview_time">${time}</div>
|
||||
</div>
|
||||
</%def>
|
|
@ -0,0 +1 @@
|
|||
window.oe_livesupport_templates_callback("\n<%def name=\"conversation\">\n <div class=\"oe_im_chatview_header\">\n <img src=\"${toUrl('../img/green.png')}\" class=\"oe_im_chatview_online\"/>\n ${widget.user.get('name')}\n <button class=\"oe_im_chatview_close\">\u00d7</button>\n </div>\n <div class=\"oe_im_chatview_disconnected\">\n ${widget.user.get(\"name\") + \" is offline. He/She will receive your messages on his/her next connection.\"}\n </div>\n <div class=\"oe_im_chatview_content\">\n <div></div>\n </div>\n <div class=\"oe_im_chatview_footer\">\n <input class=\"oe_im_chatview_input\" placeholder=\"Say something...\" />\n </div>\n</%def>\n\n<%def name=\"conversation_bubble\">\n <div class=\"oe_im_chatview_bubble\">\n <div class=\"oe_im_chatview_clip\">\n <img class=\"oe_im_chatview_avatar\" src=\"${user.get('image_url')}\"/>\n </div>\n <div class=\"oe_im_chatview_from\">${user.get('name')}</div>\n <div class=\"oe_im_chatview_bubble_list\">\n % _.each(items, function(item) {\n <div class=\"oe_im_chatview_bubble_item\">${item}</div>\n % });\n </div>\n <div class=\"oe_im_chatview_time\">${time}</div>\n </div>\n</%def>");
|
|
@ -0,0 +1,988 @@
|
|||
/*
|
||||
Copyright (c) 2012, Nicolas Vanhoren
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
if (typeof(define) !== "undefined") { // requirejs
|
||||
define(["jquery", "underscore"], nova_declare);
|
||||
} else if (typeof(exports) !== "undefined") { // node
|
||||
var _ = require("underscore")
|
||||
_.extend(exports, nova_declare(null, _));
|
||||
} else { // define global variable 'nova'
|
||||
nova = nova_declare($, _);
|
||||
}
|
||||
|
||||
function nova_declare($, _) {
|
||||
var nova = {};
|
||||
nova.internal = {};
|
||||
|
||||
/*
|
||||
* Modified Armin Ronacher's Classy library.
|
||||
*
|
||||
* Defines The Class object. That object can be used to define and inherit classes using
|
||||
* the $extend() method.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var Person = nova.Class.$extend({
|
||||
* __init__: function(isDancing){
|
||||
* this.dancing = isDancing;
|
||||
* },
|
||||
* dance: function(){
|
||||
* return this.dancing;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* The __init__() method act as a constructor. This class can be instancied this way:
|
||||
*
|
||||
* var person = new Person(true);
|
||||
* person.dance();
|
||||
*
|
||||
* The Person class can also be extended again:
|
||||
*
|
||||
* var Ninja = Person.$extend({
|
||||
* __init__: function(){
|
||||
* this.$super( false );
|
||||
* },
|
||||
* dance: function(){
|
||||
* // Call the inherited version of dance()
|
||||
* return this.$super();
|
||||
* },
|
||||
* swingSword: function(){
|
||||
* return true;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* When extending a class, each re-defined method can use this.$super() to call the previous
|
||||
* implementation of that method.
|
||||
*/
|
||||
/**
|
||||
* Classy - classy classes for JavaScript
|
||||
*
|
||||
* :copyright: (c) 2011 by Armin Ronacher.
|
||||
* :license: BSD.
|
||||
*/
|
||||
(function(){
|
||||
var
|
||||
context = this,
|
||||
disable_constructor = false;
|
||||
|
||||
/* we check if $super is in use by a class if we can. But first we have to
|
||||
check if the JavaScript interpreter supports that. This also matches
|
||||
to false positives later, but that does not do any harm besides slightly
|
||||
slowing calls down. */
|
||||
var probe_super = (function(){this.$super();}).toString().indexOf('$super') > 0;
|
||||
function usesSuper(obj) {
|
||||
return !probe_super || /\B\$super\b/.test(obj.toString());
|
||||
}
|
||||
|
||||
/* helper function to set the attribute of something to a value or
|
||||
removes it if the value is undefined. */
|
||||
function setOrUnset(obj, key, value) {
|
||||
if (value === undefined)
|
||||
delete obj[key];
|
||||
else
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
/* gets the own property of an object */
|
||||
function getOwnProperty(obj, name) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, name)
|
||||
? obj[name] : undefined;
|
||||
}
|
||||
|
||||
/* instanciate a class without calling the constructor */
|
||||
function cheapNew(cls) {
|
||||
disable_constructor = true;
|
||||
var rv = new cls;
|
||||
disable_constructor = false;
|
||||
return rv;
|
||||
}
|
||||
|
||||
/* the base class we export */
|
||||
var Class = function() {};
|
||||
|
||||
/* extend functionality */
|
||||
Class.$extend = function(properties) {
|
||||
var super_prototype = this.prototype;
|
||||
|
||||
/* disable constructors and instanciate prototype. Because the
|
||||
prototype can't raise an exception when created, we are safe
|
||||
without a try/finally here. */
|
||||
var prototype = cheapNew(this);
|
||||
|
||||
/* copy all properties of the includes over if there are any */
|
||||
prototype.__mixin_ids = _.clone(prototype.__mixin_ids || {});
|
||||
if (properties.__include__)
|
||||
for (var i = 0, n = properties.__include__.length; i != n; ++i) {
|
||||
var mixin = properties.__include__[i];
|
||||
if (mixin instanceof nova.Mixin) {
|
||||
_.extend(prototype.__mixin_ids, mixin.__mixin_ids);
|
||||
mixin = mixin.__mixin_properties;
|
||||
}
|
||||
for (var name in mixin) {
|
||||
var value = getOwnProperty(mixin, name);
|
||||
if (value !== undefined)
|
||||
prototype[name] = mixin[name];
|
||||
}
|
||||
}
|
||||
|
||||
/* copy class vars from the superclass */
|
||||
properties.__classvars__ = properties.__classvars__ || {};
|
||||
if (prototype.__classvars__)
|
||||
for (var key in prototype.__classvars__)
|
||||
if (!properties.__classvars__[key]) {
|
||||
var value = getOwnProperty(prototype.__classvars__, key);
|
||||
properties.__classvars__[key] = value;
|
||||
}
|
||||
|
||||
/* copy all properties over to the new prototype */
|
||||
for (var name in properties) {
|
||||
var value = getOwnProperty(properties, name);
|
||||
if (name === '__include__' ||
|
||||
value === undefined)
|
||||
continue;
|
||||
|
||||
prototype[name] = typeof value === 'function' && usesSuper(value) ?
|
||||
(function(meth, name) {
|
||||
return function() {
|
||||
var old_super = getOwnProperty(this, '$super');
|
||||
this.$super = super_prototype[name];
|
||||
try {
|
||||
return meth.apply(this, arguments);
|
||||
}
|
||||
finally {
|
||||
setOrUnset(this, '$super', old_super);
|
||||
}
|
||||
};
|
||||
})(value, name) : value
|
||||
}
|
||||
|
||||
var class_init = this.__class_init__ || function() {};
|
||||
var p_class_init = prototype.__class_init__ || function() {};
|
||||
delete prototype.__class_init__;
|
||||
var n_class_init = function() {
|
||||
class_init.apply(null, arguments);
|
||||
p_class_init.apply(null, arguments);
|
||||
}
|
||||
n_class_init(prototype);
|
||||
|
||||
/* dummy constructor */
|
||||
var instance = function() {
|
||||
if (disable_constructor)
|
||||
return;
|
||||
var proper_this = context === this ? cheapNew(arguments.callee) : this;
|
||||
if (proper_this.__init__)
|
||||
proper_this.__init__.apply(proper_this, arguments);
|
||||
proper_this.$class = instance;
|
||||
return proper_this;
|
||||
}
|
||||
|
||||
/* copy all class vars over of any */
|
||||
for (var key in properties.__classvars__) {
|
||||
var value = getOwnProperty(properties.__classvars__, key);
|
||||
if (value !== undefined)
|
||||
instance[key] = value;
|
||||
}
|
||||
|
||||
/* copy prototype and constructor over, reattach $extend and
|
||||
return the class */
|
||||
instance.prototype = prototype;
|
||||
instance.constructor = instance;
|
||||
instance.$extend = this.$extend;
|
||||
instance.$withData = this.$withData;
|
||||
instance.__class_init__ = n_class_init;
|
||||
return instance;
|
||||
};
|
||||
|
||||
/* instanciate with data functionality */
|
||||
Class.$withData = function(data) {
|
||||
var rv = cheapNew(this);
|
||||
for (var key in data) {
|
||||
var value = getOwnProperty(data, key);
|
||||
if (value !== undefined)
|
||||
rv[key] = value;
|
||||
}
|
||||
return rv;
|
||||
};
|
||||
|
||||
/* export the class */
|
||||
this.Class = Class;
|
||||
}).call(nova);
|
||||
// end of Armin Ronacher's code
|
||||
|
||||
var mixinId = 1;
|
||||
nova.Mixin = nova.Class.$extend({
|
||||
__init__: function() {
|
||||
this.__mixin_properties = {};
|
||||
this.__mixin_id = mixinId;
|
||||
mixinId++;
|
||||
this.__mixin_ids = {};
|
||||
this.__mixin_ids[this.__mixin_id] = true;
|
||||
_.each(_.toArray(arguments), function(el) {
|
||||
if (el instanceof nova.Mixin) {
|
||||
_.extend(this.__mixin_properties, el.__mixin_properties);
|
||||
_.extend(this.__mixin_ids, el.__mixin_ids);
|
||||
} else { // object
|
||||
_.extend(this.__mixin_properties, el)
|
||||
}
|
||||
}, this);
|
||||
_.extend(this, this.__mixin_properties);
|
||||
}
|
||||
});
|
||||
|
||||
nova.Interface = nova.Mixin.$extend({
|
||||
__init__: function() {
|
||||
var lst = [];
|
||||
_.each(_.toArray(arguments), function(el) {
|
||||
if (el instanceof nova.Interface) {
|
||||
lst.push(el);
|
||||
} else if (el instanceof nova.Mixin) {
|
||||
var tmp = new nova.Interface(el.__mixin_properties);
|
||||
tmp.__mixin_ids = el.__mixin_ids;
|
||||
lst.push(tmp);
|
||||
} else { // object
|
||||
var nprops = {};
|
||||
_.each(el, function(v, k) {
|
||||
nprops[k] = function() {
|
||||
throw new nova.NotImplementedError();
|
||||
};
|
||||
});
|
||||
lst.push(nprops);
|
||||
}
|
||||
});
|
||||
this.$super.apply(this, lst);
|
||||
}
|
||||
});
|
||||
|
||||
nova.hasMixin = function(object, mixin) {
|
||||
if (! object)
|
||||
return false;
|
||||
return (object.__mixin_ids || {})[mixin.__mixin_id] === true;
|
||||
};
|
||||
|
||||
var ErrorBase = function() {
|
||||
};
|
||||
ErrorBase.prototype = new Error();
|
||||
ErrorBase.$extend = nova.Class.$extend;
|
||||
ErrorBase.$withData = nova.Class.$withData;
|
||||
|
||||
nova.Error = ErrorBase.$extend({
|
||||
name: "nova.Error",
|
||||
defaultMessage: "",
|
||||
__init__: function(message) {
|
||||
this.message = message || this.defaultMessage;
|
||||
}
|
||||
});
|
||||
|
||||
nova.NotImplementedError = nova.Error.$extend({
|
||||
name: "nova.NotImplementedError",
|
||||
defaultMessage: "This method is not implemented"
|
||||
});
|
||||
|
||||
nova.InvalidArgumentError = nova.Error.$extend({
|
||||
name: "nova.InvalidArgumentError"
|
||||
});
|
||||
|
||||
/**
|
||||
* Mixin to express the concept of destroying an object.
|
||||
* When an object is destroyed, it should release any resource
|
||||
* it could have reserved before.
|
||||
*/
|
||||
nova.Destroyable = new nova.Mixin({
|
||||
__init__: function() {
|
||||
this.__destroyableDestroyed = false;
|
||||
},
|
||||
/**
|
||||
* Returns true if destroy() was called on the current object.
|
||||
*/
|
||||
isDestroyed : function() {
|
||||
return this.__destroyableDestroyed;
|
||||
},
|
||||
/**
|
||||
* Inform the object it should destroy itself, releasing any
|
||||
* resource it could have reserved.
|
||||
*/
|
||||
destroy : function() {
|
||||
this.__destroyableDestroyed = true;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mixin to structure objects' life-cycles folowing a parent-children
|
||||
* relationship. Each object can a have a parent and multiple children.
|
||||
* When an object is destroyed, all its children are destroyed too.
|
||||
*/
|
||||
nova.Parented = new nova.Mixin(nova.Destroyable, {
|
||||
__parentedMixin : true,
|
||||
__init__: function() {
|
||||
nova.Destroyable.__init__.apply(this);
|
||||
this.__parentedChildren = [];
|
||||
this.__parentedParent = null;
|
||||
},
|
||||
/**
|
||||
* Set the parent of the current object. When calling this method, the
|
||||
* parent will also be informed and will return the current object
|
||||
* when its getChildren() method is called. If the current object did
|
||||
* already have a parent, it is unregistered before, which means the
|
||||
* previous parent will not return the current object anymore when its
|
||||
* getChildren() method is called.
|
||||
*/
|
||||
setParent : function(parent) {
|
||||
if (this.getParent()) {
|
||||
if (this.getParent().__parentedMixin) {
|
||||
this.getParent().__parentedChildren = _.without(this
|
||||
.getParent().getChildren(), this);
|
||||
}
|
||||
}
|
||||
this.__parentedParent = parent;
|
||||
if (parent && parent.__parentedMixin) {
|
||||
parent.__parentedChildren.push(this);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Return the current parent of the object (or null).
|
||||
*/
|
||||
getParent : function() {
|
||||
return this.__parentedParent;
|
||||
},
|
||||
/**
|
||||
* Return a list of the children of the current object.
|
||||
*/
|
||||
getChildren : function() {
|
||||
return _.clone(this.__parentedChildren);
|
||||
},
|
||||
destroy : function() {
|
||||
_.each(this.getChildren(), function(el) {
|
||||
el.destroy();
|
||||
});
|
||||
this.setParent(undefined);
|
||||
nova.Destroyable.destroy.apply(this);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Yes, we steal Backbone's events :)
|
||||
*
|
||||
* This class just handle the dispatching of events, it is not meant to be extended,
|
||||
* nor used directly. All integration with parenting and automatic unregistration of
|
||||
* events is done in the mixin EventDispatcher.
|
||||
*/
|
||||
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Backbone may be freely distributed under the MIT license.
|
||||
// For all details and documentation:
|
||||
// http://backbonejs.org
|
||||
nova.internal.Events = nova.Class.$extend({
|
||||
on : function(events, callback, context) {
|
||||
var ev;
|
||||
events = events.split(/\s+/);
|
||||
var calls = this._callbacks || (this._callbacks = {});
|
||||
while (ev = events.shift()) {
|
||||
var list = calls[ev] || (calls[ev] = {});
|
||||
var tail = list.tail || (list.tail = list.next = {});
|
||||
tail.callback = callback;
|
||||
tail.context = context;
|
||||
list.tail = tail.next = {};
|
||||
}
|
||||
return this;
|
||||
},
|
||||
off : function(events, callback, context) {
|
||||
var ev, calls, node;
|
||||
if (!events) {
|
||||
delete this._callbacks;
|
||||
} else if (calls = this._callbacks) {
|
||||
events = events.split(/\s+/);
|
||||
while (ev = events.shift()) {
|
||||
node = calls[ev];
|
||||
delete calls[ev];
|
||||
if (!callback || !node)
|
||||
continue;
|
||||
while ((node = node.next) && node.next) {
|
||||
if (node.callback === callback
|
||||
&& (!context || node.context === context))
|
||||
continue;
|
||||
this.on(ev, node.callback, node.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
callbackList: function() {
|
||||
var lst = [];
|
||||
_.each(this._callbacks || {}, function(el, eventName) {
|
||||
var node = el;
|
||||
while ((node = node.next) && node.next) {
|
||||
lst.push([eventName, node.callback, node.context]);
|
||||
}
|
||||
});
|
||||
return lst;
|
||||
},
|
||||
trigger : function(events) {
|
||||
var event, node, calls, tail, args, all, rest;
|
||||
if (!(calls = this._callbacks))
|
||||
return this;
|
||||
all = calls['all'];
|
||||
(events = events.split(/\s+/)).push(null);
|
||||
// Save references to the current heads & tails.
|
||||
while (event = events.shift()) {
|
||||
if (all)
|
||||
events.push({
|
||||
next : all.next,
|
||||
tail : all.tail,
|
||||
event : event
|
||||
});
|
||||
if (!(node = calls[event]))
|
||||
continue;
|
||||
events.push({
|
||||
next : node.next,
|
||||
tail : node.tail
|
||||
});
|
||||
}
|
||||
rest = Array.prototype.slice.call(arguments, 1);
|
||||
while (node = events.pop()) {
|
||||
tail = node.tail;
|
||||
args = node.event ? [ node.event ].concat(rest) : rest;
|
||||
while ((node = node.next) !== tail) {
|
||||
node.callback.apply(node.context || this, args);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
// end of Backbone's events class
|
||||
|
||||
nova.EventDispatcher = new nova.Mixin(nova.Parented, {
|
||||
__eventDispatcherMixin: true,
|
||||
__init__: function() {
|
||||
nova.Parented.__init__.apply(this);
|
||||
this.__edispatcherEvents = new nova.internal.Events();
|
||||
this.__edispatcherRegisteredEvents = [];
|
||||
},
|
||||
on: function(events, dest, func) {
|
||||
var self = this;
|
||||
events = events.split(/\s+/);
|
||||
_.each(events, function(eventName) {
|
||||
self.__edispatcherEvents.on(eventName, func, dest);
|
||||
if (dest && dest.__eventDispatcherMixin) {
|
||||
dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
off: function(events, dest, func) {
|
||||
var self = this;
|
||||
events = events.split(/\s+/);
|
||||
_.each(events, function(eventName) {
|
||||
self.__edispatcherEvents.off(eventName, func, dest);
|
||||
if (dest && dest.__eventDispatcherMixin) {
|
||||
dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
|
||||
return !(el.name === eventName && el.func === func && el.source === self);
|
||||
});
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
trigger: function(events) {
|
||||
this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
|
||||
return this;
|
||||
},
|
||||
destroy: function() {
|
||||
var self = this;
|
||||
_.each(this.__edispatcherRegisteredEvents, function(event) {
|
||||
event.source.__edispatcherEvents.off(event.name, event.func, self);
|
||||
});
|
||||
this.__edispatcherRegisteredEvents = [];
|
||||
_.each(this.__edispatcherEvents.callbackList(), function(cal) {
|
||||
this.off(cal[0], cal[2], cal[1]);
|
||||
}, this);
|
||||
this.__edispatcherEvents.off();
|
||||
nova.Parented.destroy.apply(this);
|
||||
}
|
||||
});
|
||||
|
||||
nova.Properties = new nova.Mixin(nova.EventDispatcher, {
|
||||
__class_init__: function(proto) {
|
||||
var props = {};
|
||||
_.each(proto.__properties || {}, function(v, k) {
|
||||
props[k] = _.clone(v);
|
||||
});
|
||||
_.each(proto, function(v, k) {
|
||||
if (typeof v === "function") {
|
||||
var res = /^((?:get)|(?:set))([A-Z]\w*)$/.exec(k);
|
||||
if (! res)
|
||||
return;
|
||||
var name = res[2][0].toLowerCase() + res[2].slice(1);
|
||||
var prop = props[name] || (props[name] = {});
|
||||
prop[res[1]] = v;
|
||||
}
|
||||
});
|
||||
proto.__properties = props;
|
||||
},
|
||||
__init__: function() {
|
||||
nova.EventDispatcher.__init__.apply(this);
|
||||
this.__dynamicProperties = {};
|
||||
},
|
||||
set: function(arg1, arg2) {
|
||||
var self = this;
|
||||
var map;
|
||||
if (typeof arg1 === "string") {
|
||||
map = {};
|
||||
map[arg1] = arg2;
|
||||
} else {
|
||||
map = arg1;
|
||||
}
|
||||
var tmp_set = this.__props_setting;
|
||||
this.__props_setting = false;
|
||||
_.each(map, function(val, key) {
|
||||
var prop = self.__properties[key];
|
||||
if (prop) {
|
||||
if (! prop.set)
|
||||
throw new nova.InvalidArgumentError("Property " + key + " does not have a setter method.");
|
||||
prop.set.call(self, val);
|
||||
} else {
|
||||
self.fallbackSet(key, val);
|
||||
}
|
||||
});
|
||||
this.__props_setting = tmp_set;
|
||||
if (! this.__props_setting && this.__props_setted) {
|
||||
this.__props_setted = false;
|
||||
self.trigger("change", self);
|
||||
}
|
||||
},
|
||||
get: function(key) {
|
||||
var prop = this.__properties[key];
|
||||
if (prop) {
|
||||
if (! prop.get)
|
||||
throw new nova.InvalidArgumentError("Property " + key + " does not have a getter method.");
|
||||
return prop.get.call(this);
|
||||
} else {
|
||||
return this.fallbackGet(key);
|
||||
}
|
||||
},
|
||||
fallbackSet: function(key, val) {
|
||||
throw new nova.InvalidArgumentError("Property " + key + " is not defined.");
|
||||
},
|
||||
fallbackGet: function(key) {
|
||||
throw new nova.InvalidArgumentError("Property " + key + " is not defined.");
|
||||
},
|
||||
trigger: function(name) {
|
||||
nova.EventDispatcher.trigger.apply(this, arguments);
|
||||
if (/(\s|^)change\:.*/.exec(name)) {
|
||||
if (! this.__props_setting)
|
||||
this.trigger("change");
|
||||
else
|
||||
this.__props_setted = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
nova.DynamicProperties = new nova.Mixin(nova.Properties, {
|
||||
__init__: function() {
|
||||
nova.Properties.__init__.apply(this);
|
||||
this.__dynamicProperties = {};
|
||||
},
|
||||
fallbackSet: function(key, val) {
|
||||
var tmp = this.__dynamicProperties[key];
|
||||
if (tmp === val)
|
||||
return;
|
||||
this.__dynamicProperties[key] = val;
|
||||
this.trigger("change:" + key, this, {
|
||||
oldValue: tmp,
|
||||
newValue: val
|
||||
});
|
||||
},
|
||||
fallbackGet: function(key) {
|
||||
return this.__dynamicProperties[key];
|
||||
}
|
||||
});
|
||||
|
||||
nova.Widget = nova.Class.$extend({
|
||||
__include__ : [nova.DynamicProperties],
|
||||
tagName: 'div',
|
||||
className: '',
|
||||
attributes: {},
|
||||
events: {},
|
||||
__init__: function(parent) {
|
||||
nova.Properties.__init__.apply(this);
|
||||
this.__widget_element = $(document.createElement(this.tagName));
|
||||
this.$().addClass(this.className);
|
||||
_.each(this.attributes, function(val, key) {
|
||||
this.$().attr(key, val);
|
||||
}, this);
|
||||
_.each(this.events, function(val, key) {
|
||||
key = key.split(" ");
|
||||
val = _.bind(typeof val === "string" ? this[val] : val, this);
|
||||
if (key.length > 1) {
|
||||
this.$().on(key[0], key[1], val);
|
||||
} else {
|
||||
this.$().on(key[0], val);
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.setParent(parent);
|
||||
},
|
||||
$: function(attr) {
|
||||
if (attr)
|
||||
return this.__widget_element.find.apply(this.__widget_element, arguments);
|
||||
else
|
||||
return this.__widget_element;
|
||||
},
|
||||
/**
|
||||
* Destroys the current widget, also destroys all its children before destroying itself.
|
||||
*/
|
||||
destroy: function() {
|
||||
_.each(this.getChildren(), function(el) {
|
||||
el.destroy();
|
||||
});
|
||||
this.$().remove();
|
||||
nova.Properties.destroy.apply(this);
|
||||
},
|
||||
/**
|
||||
* Renders the current widget and appends it to the given jQuery object or Widget.
|
||||
*
|
||||
* @param target A jQuery object or a Widget instance.
|
||||
*/
|
||||
appendTo: function(target) {
|
||||
this.$().appendTo(target);
|
||||
return this.render();
|
||||
},
|
||||
/**
|
||||
* Renders the current widget and prepends it to the given jQuery object or Widget.
|
||||
*
|
||||
* @param target A jQuery object or a Widget instance.
|
||||
*/
|
||||
prependTo: function(target) {
|
||||
this.$().prependTo(target);
|
||||
return this.render();
|
||||
},
|
||||
/**
|
||||
* Renders the current widget and inserts it after to the given jQuery object or Widget.
|
||||
*
|
||||
* @param target A jQuery object or a Widget instance.
|
||||
*/
|
||||
insertAfter: function(target) {
|
||||
this.$().insertAfter(target);
|
||||
return this.render();
|
||||
},
|
||||
/**
|
||||
* Renders the current widget and inserts it before to the given jQuery object or Widget.
|
||||
*
|
||||
* @param target A jQuery object or a Widget instance.
|
||||
*/
|
||||
insertBefore: function(target) {
|
||||
this.$().insertBefore(target);
|
||||
return this.render();
|
||||
},
|
||||
/**
|
||||
* Renders the current widget and replaces the given jQuery object.
|
||||
*
|
||||
* @param target A jQuery object or a Widget instance.
|
||||
*/
|
||||
replace: function(target) {
|
||||
this.$().replace(target);
|
||||
return this.render();
|
||||
},
|
||||
/**
|
||||
* This is the method to implement to render the Widget.
|
||||
*/
|
||||
render: function() {}
|
||||
});
|
||||
|
||||
/*
|
||||
Nova Template Engine
|
||||
*/
|
||||
var escape_ = function(text) {
|
||||
return JSON.stringify(text);
|
||||
}
|
||||
var indent_ = function(txt) {
|
||||
var tmp = _.map(txt.split("\n"), function(x) { return " " + x; });
|
||||
tmp.pop();
|
||||
tmp.push("");
|
||||
return tmp.join("\n");
|
||||
};
|
||||
var tparams = {
|
||||
def_begin: /<%\s*def\s+(?:name=(?:(?:"(.+?)")|(?:'(.+?)')))\s*>/g,
|
||||
def_end: /<\/%\s*def\s*>/g,
|
||||
comment_multi_begin: /<%\s*doc\s*>/g,
|
||||
comment_multi_end: /<\/%\s*doc\s*>/g,
|
||||
eval_long_begin: /<%/g,
|
||||
eval_long_end: /%>/g,
|
||||
eval_short_begin: /(?:^|\n)[[ \t]*%(?!{)/g,
|
||||
eval_short_end: /\n|$/g,
|
||||
escape_begin: /\${/g,
|
||||
interpolate_begin: /%{/g,
|
||||
comment_begin: /##/g,
|
||||
comment_end: /\n|$/g
|
||||
};
|
||||
// /<%\s*def\s+(?:name=(?:"(.+?)"))\s*%>([\s\S]*?)<%\s*def\s*%>/g
|
||||
var allbegin = new RegExp(
|
||||
"((?:\\\\)*)(" +
|
||||
"(" + tparams.def_begin.source + ")|" +
|
||||
"(" + tparams.def_end.source + ")|" +
|
||||
"(" + tparams.comment_multi_begin.source + ")|" +
|
||||
"(" + tparams.eval_long_begin.source + ")|" +
|
||||
"(" + tparams.interpolate_begin.source + ")|" +
|
||||
"(" + tparams.eval_short_begin.source + ")|" +
|
||||
"(" + tparams.escape_begin.source + ")|" +
|
||||
"(" + tparams.comment_begin.source + ")" +
|
||||
")"
|
||||
, "g");
|
||||
allbegin.global = true;
|
||||
var regexes = {
|
||||
slashes: 1,
|
||||
match: 2,
|
||||
def_begin: 3,
|
||||
def_name1: 4,
|
||||
def_name2: 5,
|
||||
def_end: 6,
|
||||
comment_multi_begin: 7,
|
||||
eval_long: 8,
|
||||
interpolate: 9,
|
||||
eval_short: 10,
|
||||
escape: 11,
|
||||
comment: 12
|
||||
};
|
||||
var regex_count = 4;
|
||||
|
||||
var compileTemplate = function(text, options) {
|
||||
options = _.extend({start: 0, indent: true}, options);
|
||||
start = options.start;
|
||||
var source = "";
|
||||
var current = start;
|
||||
allbegin.lastIndex = current;
|
||||
var text_end = text.length;
|
||||
var restart = end;
|
||||
var found;
|
||||
var functions = [];
|
||||
var indent = options.indent ? indent_ : function (txt) { return txt; };
|
||||
var rmWhite = options.removeWhitespaces ? function(txt) {
|
||||
if (! txt)
|
||||
return txt;
|
||||
txt = _.map(txt.split("\n"), function(x) { return x.trim() });
|
||||
var last = txt.pop();
|
||||
txt = _.reject(txt, function(x) { return !x });
|
||||
txt.push(last);
|
||||
return txt.join("\n") || "\n";
|
||||
} : function(x) { return x };
|
||||
while (found = allbegin.exec(text)) {
|
||||
var to_add = rmWhite(text.slice(current, found.index));
|
||||
source += to_add ? "__p+=" + escape_(to_add) + ";\n" : '';
|
||||
current = found.index;
|
||||
|
||||
// slash escaping handling
|
||||
var slashes = found[regexes.slashes] || "";
|
||||
var nbr = slashes.length;
|
||||
var nslash = slashes.slice(0, Math.floor(nbr / 2));
|
||||
source += nbr !== 0 ? "__p+=" + escape_(nslash) + ";\n" : "";
|
||||
if (nbr % 2 !== 0) {
|
||||
source += "__p+=" + escape_(found[regexes.match]) + ";\n";
|
||||
current = found.index + found[0].length;
|
||||
allbegin.lastIndex = current;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (found[regexes.def_begin]) {
|
||||
var sub_compile = compileTemplate(text, _.extend({}, options, {start: found.index + found[0].length}));
|
||||
var name = (found[regexes.def_name1] || found[regexes.def_name2]);
|
||||
source += "var " + name + " = function(context) {\n" + indent(sub_compile.header + sub_compile.source
|
||||
+ sub_compile.footer) + "}\n";
|
||||
functions.push(name);
|
||||
current = sub_compile.end;
|
||||
} else if (found[regexes.def_end]) {
|
||||
text_end = found.index;
|
||||
restart = found.index + found[0].length;
|
||||
break;
|
||||
} else if (found[regexes.comment_multi_begin]) {
|
||||
tparams.comment_multi_end.lastIndex = found.index + found[0].length;
|
||||
var end = tparams.comment_multi_end.exec(text);
|
||||
if (!end)
|
||||
throw new Error("<%doc> without corresponding </%doc>");
|
||||
current = end.index + end[0].length;
|
||||
} else if (found[regexes.eval_long]) {
|
||||
tparams.eval_long_end.lastIndex = found.index + found[0].length;
|
||||
var end = tparams.eval_long_end.exec(text);
|
||||
if (!end)
|
||||
throw new Error("<% without matching %>");
|
||||
var code = text.slice(found.index + found[0].length, end.index);
|
||||
code = _(code.split("\n")).chain().map(function(x) { return x.trim() })
|
||||
.reject(function(x) { return !x }).value().join("\n");
|
||||
source += code + "\n";
|
||||
current = end.index + end[0].length;
|
||||
} else if (found[regexes.interpolate]) {
|
||||
var braces = /{|}/g;
|
||||
braces.lastIndex = found.index + found[0].length;
|
||||
var b_count = 1;
|
||||
var brace;
|
||||
while (brace = braces.exec(text)) {
|
||||
if (brace[0] === "{")
|
||||
b_count++;
|
||||
else {
|
||||
b_count--;
|
||||
}
|
||||
if (b_count === 0)
|
||||
break;
|
||||
}
|
||||
if (b_count !== 0)
|
||||
throw new Error("%{ without a matching }");
|
||||
source += "__p+=" + text.slice(found.index + found[0].length, brace.index) + ";\n"
|
||||
current = brace.index + brace[0].length;
|
||||
} else if (found[regexes.eval_short]) {
|
||||
tparams.eval_short_end.lastIndex = found.index + found[0].length;
|
||||
var end = tparams.eval_short_end.exec(text);
|
||||
if (!end)
|
||||
throw new Error("impossible state!!");
|
||||
source += text.slice(found.index + found[0].length, end.index).trim() + "\n";
|
||||
current = end.index;
|
||||
} else if (found[regexes.escape]) {
|
||||
var braces = /{|}/g;
|
||||
braces.lastIndex = found.index + found[0].length;
|
||||
var b_count = 1;
|
||||
var brace;
|
||||
while (brace = braces.exec(text)) {
|
||||
if (brace[0] === "{")
|
||||
b_count++;
|
||||
else {
|
||||
b_count--;
|
||||
}
|
||||
if (b_count === 0)
|
||||
break;
|
||||
}
|
||||
if (b_count !== 0)
|
||||
throw new Error("${ without a matching }");
|
||||
source += "__p+=_.escape(" + text.slice(found.index + found[0].length, brace.index) + ");\n"
|
||||
current = brace.index + brace[0].length;
|
||||
} else { // comment
|
||||
tparams.comment_end.lastIndex = found.index + found[0].length;
|
||||
var end = tparams.comment_end.exec(text);
|
||||
if (!end)
|
||||
throw new Error("impossible state!!");
|
||||
current = end.index + end[0].length;
|
||||
}
|
||||
allbegin.lastIndex = current;
|
||||
}
|
||||
var to_add = rmWhite(text.slice(current, text_end));
|
||||
source += to_add ? "__p+=" + escape_(to_add) + ";\n" : "";
|
||||
|
||||
var header = "var __p = ''; var print = function() { __p+=Array.prototype.join.call(arguments, '') };\n" +
|
||||
"with (context || {}) {\n";
|
||||
var footer = "}\nreturn __p;\n";
|
||||
source = indent(source);
|
||||
|
||||
return {
|
||||
header: header,
|
||||
source: source,
|
||||
footer: footer,
|
||||
end: restart,
|
||||
functions: functions,
|
||||
};
|
||||
};
|
||||
|
||||
nova.TemplateEngine = nova.Class.$extend({
|
||||
__init__: function() {
|
||||
this.resetEnvironment();
|
||||
this.options = {
|
||||
includeInDom: $ ? true : false,
|
||||
indent: true,
|
||||
removeWhitespaces: true,
|
||||
};
|
||||
},
|
||||
loadFile: function(filename) {
|
||||
var self = this;
|
||||
return $.get(filename).pipe(function(content) {
|
||||
return self.loadFileContent(content);
|
||||
});
|
||||
},
|
||||
loadFileContent: function(file_content) {
|
||||
var code = this.compileFile(file_content);
|
||||
|
||||
if (this.options.includeInDom) {
|
||||
var varname = _.uniqueId("novajstemplate");
|
||||
var previous = window[varname];
|
||||
code = "window." + varname + " = " + code + ";";
|
||||
var def = $.Deferred();
|
||||
var script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.text = code;
|
||||
$("head")[0].appendChild(script);
|
||||
$(script).ready(function() {
|
||||
def.resolve();
|
||||
});
|
||||
def.then(_.bind(function() {
|
||||
var tmp = window[varname];
|
||||
window[varname] = previous;
|
||||
this.includeTemplates(tmp);
|
||||
}, this));
|
||||
return def;
|
||||
} else {
|
||||
console.log("return (" + code + ")(context);");
|
||||
return this.includeTemplates(new Function('context', "return (" + code + ")(context);"));
|
||||
}
|
||||
},
|
||||
compileFile: function(file_content) {
|
||||
var result = compileTemplate(file_content, _.extend({}, this.options));
|
||||
var to_append = "";
|
||||
_.each(result.functions, function(name) {
|
||||
to_append += name + ": " + name + ",\n";
|
||||
}, this);
|
||||
to_append = this.options.indent ? indent_(to_append) : to_append;
|
||||
to_append = "return {\n" + to_append + "};\n";
|
||||
to_append = this.options.indent ? indent_(to_append) : to_append;
|
||||
var code = "function(context) {\n" + result.header +
|
||||
result.source + to_append + result.footer + "}\n";
|
||||
return code;
|
||||
},
|
||||
includeTemplates: function(fct) {
|
||||
var add = _.extend({engine: this}, this._env);
|
||||
var functions = fct(add);
|
||||
_.each(functions, function(func, name) {
|
||||
if (this[name])
|
||||
throw new Error("The template '" + name + "' is already defined");
|
||||
this[name] = func;
|
||||
}, this);
|
||||
},
|
||||
buildTemplate: function(text) {
|
||||
var comp = compileTemplate(text, _.extend({}, this.options));
|
||||
var result = comp.header + comp.source + comp.footer;
|
||||
var add = _.extend({engine: this}, this._env);
|
||||
var func = new Function('context', result);
|
||||
return function(data) {
|
||||
return func.call(this, _.extend(add, data));
|
||||
};
|
||||
},
|
||||
eval: function(text, context) {
|
||||
return this.buildTemplate(text)(context);
|
||||
},
|
||||
resetEnvironment: function(nenv) {
|
||||
this._env = {_: _};
|
||||
this.extendEnvironment(nenv);
|
||||
},
|
||||
extendEnvironment: function(env) {
|
||||
_.extend(this._env, env || {});
|
||||
},
|
||||
});
|
||||
|
||||
return nova;
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,338 @@
|
|||
|
||||
define(["underscore", "jquery", "nova"], function(_, $, nova) {
|
||||
|
||||
var oeclient = {};
|
||||
|
||||
var genericJsonRPC = function(fct_name, params, fct) {
|
||||
var data = {
|
||||
jsonrpc: "2.0",
|
||||
method: fct_name,
|
||||
params: params,
|
||||
id: Math.floor(Math.random()* (1000*1000*1000)),
|
||||
};
|
||||
return fct(data).pipe(function(result) {
|
||||
if (result.error !== undefined) {
|
||||
console.error("Server application error", result.error);
|
||||
return $.Deferred().reject("server", result.error);
|
||||
} else {
|
||||
return result.result;
|
||||
}
|
||||
}, function() {
|
||||
console.error("JsonRPC communication error", _.toArray(arguments));
|
||||
var def = $.Deferred();
|
||||
return def.reject.apply(def, ["communication"].concat(_.toArray(arguments)));
|
||||
});
|
||||
};
|
||||
|
||||
oeclient.jsonRpc = function(url, fct_name, params, settings) {
|
||||
return genericJsonRPC(fct_name, params, function(data) {
|
||||
return $.ajax(url, _.extend({}, settings, {
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
contentType: 'application/json',
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
oeclient.jsonpRpc = function(url, fct_name, params, settings) {
|
||||
return genericJsonRPC(fct_name, params, function(data) {
|
||||
return $.ajax(url, _.extend({}, settings, {
|
||||
url: url,
|
||||
dataType: 'jsonp',
|
||||
jsonp: 'jsonp',
|
||||
type: 'GET',
|
||||
cache: false,
|
||||
data: {r: JSON.stringify(data)},
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
oeclient.Connector = nova.Class.$extend({
|
||||
getService: function(serviceName) {
|
||||
return new oeclient.Service(this, serviceName);
|
||||
},
|
||||
});
|
||||
|
||||
oeclient.JsonRPCConnector = oeclient.Connector.$extend({
|
||||
__init__: function(url) {
|
||||
this.url = url;
|
||||
},
|
||||
call: function(sub_url, content) {
|
||||
return oeclient.jsonRpc(this.url + sub_url, "call", content);
|
||||
},
|
||||
send: function(serviceName, method, args) {
|
||||
return this.call("/jsonrpc", {"service": serviceName, "method": method, "args": args});
|
||||
},
|
||||
});
|
||||
|
||||
oeclient.JsonpRPCConnector = oeclient.JsonRPCConnector.$extend({
|
||||
call: function(sub_url, content) {
|
||||
return oeclient.jsonpRpc(this.url + sub_url, "call", content);
|
||||
},
|
||||
});
|
||||
|
||||
oeclient.Service = nova.Class.$extend({
|
||||
__init__: function(connector, serviceName) {
|
||||
this.connector = connector;
|
||||
this.serviceName = serviceName;
|
||||
},
|
||||
call: function(method, args) {
|
||||
return this.connector.send(this.serviceName, method, args);
|
||||
},
|
||||
});
|
||||
|
||||
oeclient.AuthenticationError = nova.Error.$extend({
|
||||
name: "oeclient.AuthenticationError",
|
||||
defaultMessage: "An error occured during authentication."
|
||||
});
|
||||
|
||||
oeclient.Connection = nova.Class.$extend({
|
||||
__init__: function(connector, database, login, password, userId) {
|
||||
this.connector = connector;
|
||||
this.setLoginInfo(database, login, password, userId);
|
||||
this.userContext = null;
|
||||
},
|
||||
setLoginInfo: function(database, login, password, userId) {
|
||||
this.database = database;
|
||||
this.login = login;
|
||||
this.password = password;
|
||||
this.userId = userId;
|
||||
},
|
||||
checkLogin: function(force) {
|
||||
force = force === undefined ? true: force;
|
||||
if (this.userId && ! force)
|
||||
return $.when();
|
||||
|
||||
if (! this.database || ! this.login || ! this.password)
|
||||
throw new oeclient.AuthenticationError();
|
||||
|
||||
return this.getService("common").call("login", [this.database, this.login, this.password])
|
||||
.then(_.bind(function(result) {
|
||||
this.userId = result;
|
||||
if (! this.userId) {
|
||||
console.error("Authentication failure");
|
||||
return $.Deferred().reject({message:"Authentication failure"});
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
getUserContext: function() {
|
||||
if (! this.userContext) {
|
||||
return this.getModel("res.users").call("context_get").then(_.bind(function(result) {
|
||||
this.userContext = result;
|
||||
return this.userContext;
|
||||
}, this));
|
||||
}
|
||||
return $.when(this.userContext);
|
||||
},
|
||||
getModel: function(modelName) {
|
||||
return new oeclient.Model(this, modelName);
|
||||
},
|
||||
getService: function(serviceName) {
|
||||
return this.connector.getService(serviceName);
|
||||
},
|
||||
});
|
||||
|
||||
oeclient.Model = nova.Class.$extend({
|
||||
__init__: function(connection, modelName) {
|
||||
this.connection = connection;
|
||||
this.modelName = modelName;
|
||||
},
|
||||
call: function(method, args, kw) {
|
||||
return this.connection.checkLogin().then(_.bind(function() {
|
||||
return this.connection.getService("object").call("execute_kw", [
|
||||
this.connection.database,
|
||||
this.connection.userId,
|
||||
this.connection.password,
|
||||
this.modelName,
|
||||
method,
|
||||
args || [],
|
||||
kw || {},
|
||||
]);
|
||||
}, this));
|
||||
},
|
||||
search_read: function(domain, fields, offset, limit, order, context) {
|
||||
return this.call("search", [domain || [], offset || 0, limit || false, order || false, context || {}]).then(_.bind(function(record_ids) {
|
||||
if (! record_ids) {
|
||||
return [];
|
||||
}
|
||||
return this.call("read", [record_ids, fields || [], context || {}]).then(function(records) {
|
||||
var index = {};
|
||||
_.each(records, function(r) {
|
||||
index[r.id] = r;
|
||||
});
|
||||
var res = [];
|
||||
_.each(record_ids, function(id) {
|
||||
if (index[id])
|
||||
res.push(index[id]);
|
||||
});
|
||||
return res;
|
||||
});
|
||||
}, this));
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Converts a string to a Date javascript object using OpenERP's
|
||||
* datetime string format (exemple: '2011-12-01 15:12:35').
|
||||
*
|
||||
* The time zone is assumed to be UTC (standard for OpenERP 6.1)
|
||||
* and will be converted to the browser's time zone.
|
||||
*
|
||||
* @param {String} str A string representing a datetime.
|
||||
* @returns {Date}
|
||||
*/
|
||||
oeclient.str_to_datetime = function(str) {
|
||||
if(!str) {
|
||||
return str;
|
||||
}
|
||||
var regex = /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d(?:\.\d+)?)$/;
|
||||
var res = regex.exec(str);
|
||||
if ( !res ) {
|
||||
throw new Error("'" + str + "' is not a valid datetime");
|
||||
}
|
||||
var tmp = new Date();
|
||||
tmp.setUTCFullYear(parseFloat(res[1]));
|
||||
tmp.setUTCMonth(parseFloat(res[2]) - 1);
|
||||
tmp.setUTCDate(parseFloat(res[3]));
|
||||
tmp.setUTCHours(parseFloat(res[4]));
|
||||
tmp.setUTCMinutes(parseFloat(res[5]));
|
||||
tmp.setUTCSeconds(parseFloat(res[6]));
|
||||
return tmp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a string to a Date javascript object using OpenERP's
|
||||
* date string format (exemple: '2011-12-01').
|
||||
*
|
||||
* As a date is not subject to time zones, we assume it should be
|
||||
* represented as a Date javascript object at 00:00:00 in the
|
||||
* time zone of the browser.
|
||||
*
|
||||
* @param {String} str A string representing a date.
|
||||
* @returns {Date}
|
||||
*/
|
||||
oeclient.str_to_date = function(str) {
|
||||
if(!str) {
|
||||
return str;
|
||||
}
|
||||
var regex = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
||||
var res = regex.exec(str);
|
||||
if ( !res ) {
|
||||
throw new Error("'" + str + "' is not a valid date");
|
||||
}
|
||||
var tmp = new Date();
|
||||
tmp.setFullYear(parseFloat(res[1]));
|
||||
tmp.setMonth(parseFloat(res[2]) - 1);
|
||||
tmp.setDate(parseFloat(res[3]));
|
||||
tmp.setHours(0);
|
||||
tmp.setMinutes(0);
|
||||
tmp.setSeconds(0);
|
||||
return tmp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a string to a Date javascript object using OpenERP's
|
||||
* time string format (exemple: '15:12:35').
|
||||
*
|
||||
* The OpenERP times are supposed to always be naive times. We assume it is
|
||||
* represented using a javascript Date with a date 1 of January 1970 and a
|
||||
* time corresponding to the meant time in the browser's time zone.
|
||||
*
|
||||
* @param {String} str A string representing a time.
|
||||
* @returns {Date}
|
||||
*/
|
||||
oeclient.str_to_time = function(str) {
|
||||
if(!str) {
|
||||
return str;
|
||||
}
|
||||
var regex = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)$/;
|
||||
var res = regex.exec(str);
|
||||
if ( !res ) {
|
||||
throw new Error("'" + str + "' is not a valid time");
|
||||
}
|
||||
debugger;
|
||||
var tmp = new Date();
|
||||
tmp.setFullYear(1970);
|
||||
tmp.setMonth(0);
|
||||
tmp.setDate(1);
|
||||
tmp.setHours(parseFloat(res[1]));
|
||||
tmp.setMinutes(parseFloat(res[2]));
|
||||
tmp.setSeconds(parseFloat(res[3]));
|
||||
return tmp;
|
||||
};
|
||||
|
||||
/*
|
||||
* Left-pad provided arg 1 with zeroes until reaching size provided by second
|
||||
* argument.
|
||||
*
|
||||
* @param {Number|String} str value to pad
|
||||
* @param {Number} size size to reach on the final padded value
|
||||
* @returns {String} padded string
|
||||
*/
|
||||
var zpad = function(str, size) {
|
||||
str = "" + str;
|
||||
return new Array(size - str.length + 1).join('0') + str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Date javascript object to a string using OpenERP's
|
||||
* datetime string format (exemple: '2011-12-01 15:12:35').
|
||||
*
|
||||
* The time zone of the Date object is assumed to be the one of the
|
||||
* browser and it will be converted to UTC (standard for OpenERP 6.1).
|
||||
*
|
||||
* @param {Date} obj
|
||||
* @returns {String} A string representing a datetime.
|
||||
*/
|
||||
oeclient.datetime_to_str = function(obj) {
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
return zpad(obj.getUTCFullYear(),4) + "-" + zpad(obj.getUTCMonth() + 1,2) + "-"
|
||||
+ zpad(obj.getUTCDate(),2) + " " + zpad(obj.getUTCHours(),2) + ":"
|
||||
+ zpad(obj.getUTCMinutes(),2) + ":" + zpad(obj.getUTCSeconds(),2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Date javascript object to a string using OpenERP's
|
||||
* date string format (exemple: '2011-12-01').
|
||||
*
|
||||
* As a date is not subject to time zones, we assume it should be
|
||||
* represented as a Date javascript object at 00:00:00 in the
|
||||
* time zone of the browser.
|
||||
*
|
||||
* @param {Date} obj
|
||||
* @returns {String} A string representing a date.
|
||||
*/
|
||||
oeclient.date_to_str = function(obj) {
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
return zpad(obj.getFullYear(),4) + "-" + zpad(obj.getMonth() + 1,2) + "-"
|
||||
+ zpad(obj.getDate(),2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Date javascript object to a string using OpenERP's
|
||||
* time string format (exemple: '15:12:35').
|
||||
*
|
||||
* The OpenERP times are supposed to always be naive times. We assume it is
|
||||
* represented using a javascript Date with a date 1 of January 1970 and a
|
||||
* time corresponding to the meant time in the browser's time zone.
|
||||
*
|
||||
* @param {Date} obj
|
||||
* @returns {String} A string representing a time.
|
||||
*/
|
||||
oeclient.time_to_str = function(obj) {
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
return zpad(obj.getHours(),2) + ":" + zpad(obj.getMinutes(),2) + ":"
|
||||
+ zpad(obj.getSeconds(),2);
|
||||
};
|
||||
|
||||
return oeclient;
|
||||
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
import sys
|
||||
import json
|
||||
|
||||
file_name = sys.argv[1]
|
||||
function_name = sys.argv[2]
|
||||
|
||||
with open(file_name) as file_:
|
||||
content = file_.read()
|
||||
|
||||
print "window.%s(%s);" % (function_name, json.dumps(content))
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="http://localhost:8069/live_support/static/ext/static/js/require.js"></script>
|
||||
<script>
|
||||
require.config({
|
||||
context: "oelivesupport",
|
||||
baseUrl: "http://localhost:8069/live_support/static/ext/static/js",
|
||||
shim: {
|
||||
underscore: {
|
||||
init: function() {
|
||||
return _.noConflict();
|
||||
},
|
||||
},
|
||||
},
|
||||
})(["livesupport", "jquery"], function(livesupport, jQuery) {
|
||||
jQuery.noConflict();
|
||||
livesupport.main("http://localhost:8069", "im", "admin", "a");
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
openerp.live_support = function(instance) {
|
||||
|
||||
var _t = instance.web._t,
|
||||
_lt = instance.web._lt;
|
||||
var QWeb = instance.web.qweb;
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
</templates>
|
|
@ -28,33 +28,84 @@ from osv import osv, fields
|
|||
import time
|
||||
import logging
|
||||
import json
|
||||
import select
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
WATCHER_TIMER = 60
|
||||
WATCHER_ERROR_DELAY = 10
|
||||
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
|
||||
|
||||
if openerp.tools.config.options["gevent"]:
|
||||
import gevent
|
||||
import gevent.event
|
||||
import select
|
||||
|
||||
global Watcher
|
||||
|
||||
class Watcher:
|
||||
class ImWatcher(object):
|
||||
watchers = {}
|
||||
|
||||
@staticmethod
|
||||
def get_watcher(db_name):
|
||||
if not Watcher.watchers.get(db_name):
|
||||
Watcher(db_name)
|
||||
return Watcher.watchers[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
|
||||
Watcher.watchers[db_name] = self
|
||||
ImWatcher.watchers[db_name] = self
|
||||
self.waiting = 0
|
||||
self.wait_id = 0
|
||||
self.users = {}
|
||||
|
@ -62,44 +113,31 @@ if openerp.tools.config.options["gevent"]:
|
|||
gevent.spawn(self.loop)
|
||||
|
||||
def loop(self):
|
||||
_logger.info("Begin watching for instant messaging events for database " + self.db_name)
|
||||
stopping = False
|
||||
while not stopping:
|
||||
_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 c:
|
||||
conn = c._cnx
|
||||
try:
|
||||
c.execute("listen im_channel;")
|
||||
c.commit();
|
||||
while not stopping:
|
||||
if self.waiting == 0:
|
||||
stopping = True
|
||||
break
|
||||
if select.select([conn], [], [], WATCHER_TIMER) == ([],[],[]):
|
||||
pass
|
||||
else:
|
||||
conn.poll()
|
||||
while conn.notifies:
|
||||
message = json.loads(conn.notifies.pop().payload)
|
||||
if message["type"] == "message":
|
||||
for waiter in self.users.get(message["receiver"], {}).values():
|
||||
waiter.set()
|
||||
else: #type status
|
||||
for waiter in self.users_watch.get(message["user"], {}).values():
|
||||
waiter.set()
|
||||
finally:
|
||||
try:
|
||||
c.execute("unlisten im_channel;")
|
||||
c.commit()
|
||||
except:
|
||||
pass # can't do anything if that fails
|
||||
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 instant messaging watcher activity")
|
||||
_logger.exception("Exception during watcher activity")
|
||||
time.sleep(WATCHER_ERROR_DELAY)
|
||||
del Watcher.watchers[self.db_name]
|
||||
_logger.info("End watching for instant messaging events for database " + self.db_name)
|
||||
_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 waiter in self.users.get(message["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
|
||||
|
@ -125,9 +163,13 @@ class ImportController(openerp.addons.web.http.Controller):
|
|||
_cp_path = '/longpolling/im'
|
||||
|
||||
@openerp.addons.web.http.jsonrequest
|
||||
def poll(self, req, last=None, users_watch=None):
|
||||
def poll(self, req, last=None, users_watch=None, db=None, uid=None, password=None):
|
||||
if not openerp.tools.config.options["gevent"]:
|
||||
raise Exception("Not usable in a server not running gevent")
|
||||
if db is not None:
|
||||
req.session._db = db
|
||||
req.session._uid = uid
|
||||
req.session._password = password
|
||||
req.session.model('im.user').im_connect(context=req.context)
|
||||
num = 0
|
||||
while True:
|
||||
|
@ -136,7 +178,7 @@ class ImportController(openerp.addons.web.http.Controller):
|
|||
return res
|
||||
last = res["last"]
|
||||
num += 1
|
||||
Watcher.get_watcher(res["dbname"]).stop(req.session._uid, users_watch or [], POLL_TIMER)
|
||||
ImWatcher.get_watcher(res["dbname"]).stop(req.session._uid, users_watch or [], POLL_TIMER)
|
||||
|
||||
@openerp.addons.web.http.jsonrequest
|
||||
def activated(self, req):
|
||||
|
@ -153,7 +195,7 @@ class im_message(osv.osv):
|
|||
}
|
||||
|
||||
_defaults = {
|
||||
'date': datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
'date': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
}
|
||||
|
||||
def get_messages(self, cr, uid, last=None, users_watch=None, context=None):
|
||||
|
@ -182,9 +224,7 @@ class im_message(osv.osv):
|
|||
|
||||
def post(self, cr, uid, message, to_user_id, context=None):
|
||||
self.create(cr, uid, {"message": message, 'from': uid, 'to': to_user_id}, context=context)
|
||||
cr.commit()
|
||||
cr.execute("notify im_channel, %s", [json.dumps({'type': 'message', 'receiver': to_user_id})])
|
||||
cr.commit()
|
||||
notify_channel(cr, "im_channel", {'type': 'message', 'receiver': to_user_id})
|
||||
return False
|
||||
|
||||
class im_user(osv.osv):
|
||||
|
@ -232,7 +272,7 @@ class im_user(osv.osv):
|
|||
"im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
|
||||
cr.commit()
|
||||
if current_status != new_one:
|
||||
cr.execute("notify im_channel, %s", [json.dumps({'type': 'status', 'user': uid})])
|
||||
notify_channel(cr, "im_channel", {'type': 'status', 'user': uid})
|
||||
cr.commit()
|
||||
return True
|
||||
|
||||
|
|
|
@ -44,15 +44,12 @@ openerp.web_im = function(instance) {
|
|||
this.shown = false;
|
||||
this.set("right_offset", 0);
|
||||
this.set("current_search", "");
|
||||
this.last = null;
|
||||
this.users = [];
|
||||
this.activated = false;
|
||||
this.c_manager = new instance.web_im.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 instance.web.DropMisordered();
|
||||
this.users_cache = {};
|
||||
this.unload_event_handler = _.bind(this.unload, this);
|
||||
},
|
||||
start: function() {
|
||||
|
@ -68,17 +65,7 @@ openerp.web_im = function(instance) {
|
|||
|
||||
$(window).on("unload", this.unload_event_handler);
|
||||
|
||||
return this.ensure_users([instance.session.uid]).then(function() {
|
||||
var me = self.users_cache[instance.session.uid];
|
||||
delete self.users_cache[instance.session.uid];
|
||||
self.c_manager.set_me(me);
|
||||
self.rpc("/longpolling/im/activated", {}).then(function(activated) {
|
||||
if (activated) {
|
||||
self.activated = true;
|
||||
self.poll();
|
||||
}
|
||||
});
|
||||
});
|
||||
return this.c_manager.start_polling();
|
||||
},
|
||||
unload: function() {
|
||||
return new instance.web.Model("im.user").call("im_disconnect", [], {context: new instance.web.CompoundContext()});
|
||||
|
@ -103,12 +90,12 @@ openerp.web_im = function(instance) {
|
|||
return this.user_search_dm.add(users.call("search_users",
|
||||
[[["name", "ilike", this.get("current_search")], ["id", "<>", instance.session.uid]],
|
||||
["name"], USERS_LIMIT], {context:new instance.web.CompoundContext()})).then(function(result) {
|
||||
self.add_to_user_cache(result);
|
||||
self.c_manager.add_to_user_cache(result);
|
||||
self.$(".oe_im_input").val("");
|
||||
var old_users = self.users;
|
||||
self.users = [];
|
||||
_.each(result, function(user) {
|
||||
var widget = new instance.web_im.UserWidget(self, self.get_user(user.id));
|
||||
var widget = new instance.web_im.UserWidget(self, self.c_manager.get_user(user.id));
|
||||
widget.appendTo(self.$(".oe_im_users"));
|
||||
widget.on("activate_user", self, self.activate_user);
|
||||
self.users.push(widget);
|
||||
|
@ -118,35 +105,6 @@ openerp.web_im = function(instance) {
|
|||
});
|
||||
});
|
||||
},
|
||||
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;
|
||||
if (_.size(no_cache) === 0)
|
||||
return $.when();
|
||||
else
|
||||
return new instance.web.Model("im.user").call("read_users", [_.values(no_cache), ["name"]],
|
||||
{context: new instance.web.CompoundContext()}).then(function(users) {
|
||||
self.add_to_user_cache(users);
|
||||
});
|
||||
},
|
||||
add_to_user_cache: function(user_recs) {
|
||||
_.each(user_recs, function(user_rec) {
|
||||
if (! this.users_cache[user_rec.id]) {
|
||||
var user = new instance.web_im.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];
|
||||
},
|
||||
switch_display: function() {
|
||||
var fct = _.bind(function(place) {
|
||||
this.set("right_offset", place + this.$el.outerWidth());
|
||||
|
@ -159,7 +117,7 @@ openerp.web_im = function(instance) {
|
|||
right: -this.$el.outerWidth(),
|
||||
}, opt);
|
||||
} else {
|
||||
if (! this.activated) {
|
||||
if (! this.c_manager.get_activated()) {
|
||||
this.do_warn("Instant Messaging is not activated on this server.", "");
|
||||
return;
|
||||
}
|
||||
|
@ -169,33 +127,6 @@ openerp.web_im = function(instance) {
|
|||
}
|
||||
this.shown = ! this.shown;
|
||||
},
|
||||
poll: function() {
|
||||
var self = this;
|
||||
var user_ids = _.map(this.users_cache, function(el) {
|
||||
return el.get("id");
|
||||
});
|
||||
this.rpc("/longpolling/im/poll", {
|
||||
last: this.last,
|
||||
users_watch: user_ids,
|
||||
context: instance.web.pyeval.eval('context', {}),
|
||||
}, {shadow: true}).then(function(result) {
|
||||
_.each(result.users_status, function(el) {
|
||||
self.get_user(el.id).set(el);
|
||||
});
|
||||
self.last = result.last;
|
||||
var user_ids = _.pluck(_.pluck(result.res, "from"), 0);
|
||||
self.ensure_users(user_ids).then(function() {
|
||||
_.each(result.res, function(mes) {
|
||||
var user = self.get_user(mes.from[0]);
|
||||
self.c_manager.received_message(mes, user);
|
||||
});
|
||||
self.poll();
|
||||
});
|
||||
}, function(unused, e) {
|
||||
e.preventDefault();
|
||||
setTimeout(_.bind(self.poll, self), ERROR_DELAY);
|
||||
});
|
||||
},
|
||||
activate_user: function(user) {
|
||||
this.c_manager.activate_user(user);
|
||||
},
|
||||
|
@ -272,6 +203,83 @@ openerp.web_im = function(instance) {
|
|||
this.on("change:waiting_messages", this, this.messages_change);
|
||||
this.messages_change();
|
||||
this.create_ting();
|
||||
this.activated = false;
|
||||
this.users_cache = {};
|
||||
this.last = null;
|
||||
},
|
||||
start_polling: function() {
|
||||
var self = this;
|
||||
return this.ensure_users([instance.session.uid]).then(function() {
|
||||
var me = self.users_cache[instance.session.uid];
|
||||
delete self.users_cache[instance.session.uid];
|
||||
self.me = me;
|
||||
self.rpc("/longpolling/im/activated", {}).then(function(activated) {
|
||||
if (activated) {
|
||||
self.activated = true;
|
||||
self.poll();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
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;
|
||||
if (_.size(no_cache) === 0)
|
||||
return $.when();
|
||||
else
|
||||
return new instance.web.Model("im.user").call("read_users", [_.values(no_cache), ["name"]],
|
||||
{context: new instance.web.CompoundContext()}).then(function(users) {
|
||||
self.add_to_user_cache(users);
|
||||
});
|
||||
},
|
||||
add_to_user_cache: function(user_recs) {
|
||||
_.each(user_recs, function(user_rec) {
|
||||
if (! this.users_cache[user_rec.id]) {
|
||||
var user = new instance.web_im.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");
|
||||
});
|
||||
this.rpc("/longpolling/im/poll", {
|
||||
last: this.last,
|
||||
users_watch: user_ids,
|
||||
context: instance.web.pyeval.eval('context', {}),
|
||||
}, {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;
|
||||
var user_ids = _.pluck(_.pluck(result.res, "from"), 0);
|
||||
self.ensure_users(user_ids).then(function() {
|
||||
_.each(result.res, function(mes) {
|
||||
var user = self.get_user(mes.from[0]);
|
||||
self.received_message(mes, user);
|
||||
});
|
||||
self.poll();
|
||||
});
|
||||
}, function(unused, e) {
|
||||
e.preventDefault();
|
||||
setTimeout(_.bind(self.poll, self), ERROR_DELAY);
|
||||
});
|
||||
},
|
||||
get_activated: function() {
|
||||
return this.activated;
|
||||
},
|
||||
create_ting: function() {
|
||||
var kitten = jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
|
||||
|
@ -289,9 +297,6 @@ openerp.web_im = function(instance) {
|
|||
instance.webclient.set_title_part("im_messages", this.get("waiting_messages") === 0 ? undefined :
|
||||
_.str.sprintf(_t("%d Messages"), this.get("waiting_messages")));
|
||||
},
|
||||
set_me: function(me) {
|
||||
this.me = me;
|
||||
},
|
||||
activate_user: function(user) {
|
||||
if (this.users[user.get('id')]) {
|
||||
return this.users[user.get('id')];
|
||||
|
|