merge live support branch

bzr revid: nicolas.vanhoren@openerp.com-20130129134439-9rca0b38f2eqd39q
This commit is contained in:
niv-openerp 2013-01-29 14:44:39 +01:00
commit 8d881beaa8
30 changed files with 14883 additions and 126 deletions

View File

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

View File

@ -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,
}

View File

@ -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/>.
#
##############################################################################

View File

@ -0,0 +1 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink

View File

@ -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

Binary file not shown.

Binary file not shown.

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

File diff suppressed because it is too large Load Diff

View File

@ -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;
});

View File

@ -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>

View File

@ -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>");

View File

@ -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;
};
})();

View File

@ -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;
});

File diff suppressed because it is too large Load Diff

View File

@ -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))

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -0,0 +1,8 @@
openerp.live_support = function(instance) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
</templates>

View File

@ -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

View File

@ -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')];