[MERGE] Merge with trunk upto revision no 1218.
[odoo/odoo.git] / addons / web / static / src / js / core.js
index 78fb176..b1708bb 100644 (file)
@@ -1,8 +1,17 @@
 /*---------------------------------------------------------
  * OpenERP Web core
  *--------------------------------------------------------*/
+var console;
+if (!console) {
+    console = {log: function () {}};
+}
+if (!console.debug) {
+    console.debug = console.log;
+}
 
 openerp.web.core = function(openerp) {
+openerp.web.qweb = new QWeb2.Engine();
+openerp.web.qweb.debug = (window.location.search.indexOf('?debug') !== -1);
 /**
  * John Resig Class with factory improvement
  */
@@ -11,11 +20,17 @@ openerp.web.core = function(openerp) {
         fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
     // The web Class implementation (does nothing)
     /**
+     * Extended version of John Resig's Class pattern
+     *
      * @class
      */
     openerp.web.Class = function(){};
 
-    // Create a new Class that inherits from this class
+    /**
+     * Subclass an existing class
+     *
+     * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
+     */
     openerp.web.Class.extend = function(prop) {
         var _super = this.prototype;
 
@@ -59,6 +74,7 @@ openerp.web.core = function(openerp) {
             }
             return this;
         }
+        // This should NOT be used, like callbackenable it's too hackish not enough javasish
         Class.include = function (properties) {
             for (var name in properties) {
                 if (typeof properties[name] !== 'function'
@@ -156,7 +172,7 @@ openerp.web.callback = function(obj, method) {
  * that does nothing and always return undefined).
  *
  * @param {Class} claz
- * @param {dict} add Additional functions to override.
+ * @param {Object} add Additional functions to override.
  * @return {Class}
  */
 openerp.web.generate_null_object_class = function(claz, add) {
@@ -187,7 +203,7 @@ openerp.web.KeyNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.Ke
     /**
      * Thrown when a key could not be found in a mapping
      *
-     * @constructs
+     * @constructs openerp.web.KeyNotFound
      * @extends openerp.web.NotFound
      * @param {String} key the key which could not be found
      */
@@ -203,7 +219,7 @@ openerp.web.ObjectNotFound = openerp.web.NotFound.extend( /** @lends openerp.web
      * Thrown when an object path does not designate a valid class or object
      * in the openerp hierarchy.
      *
-     * @constructs
+     * @constructs openerp.web.ObjectNotFound
      * @extends openerp.web.NotFound
      * @param {String} path the invalid object path
      */
@@ -227,7 +243,7 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry
      * object pointed to (e.g. ``"openerp.web.Session"`` for an OpenERP
      * session object).
      *
-     * @constructs
+     * @constructs openerp.web.Registry
      * @param {Object} mapping a mapping of keys to object-paths
      */
     init: function (mapping) {
@@ -308,7 +324,11 @@ openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry
     }
 });
 
-openerp.web.CallbackEnabled = openerp.web.Class.extend({
+openerp.web.CallbackEnabled = openerp.web.Class.extend(/** @lends openerp.web.CallbackEnabled# */{
+    /**
+     * @constructs openerp.web.CallbackEnabled
+     * @extends openerp.web.Class
+     */
     init: function() {
         // Transform on_* method into openerp.web.callbacks
         for (var name in this) {
@@ -325,9 +345,11 @@ openerp.web.CallbackEnabled = openerp.web.Class.extend({
 
 openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web.Session# */{
     /**
-     * @constructs
-     * @param server
-     * @param port
+     * @constructs openerp.web.Session
+     * @extends openerp.web.CallbackEnabled
+     *
+     * @param {String} [server] JSON-RPC endpoint hostname
+     * @param {String} [port] JSON-RPC endpoint port
      */
     init: function(server, port) {
         this._super();
@@ -335,18 +357,15 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
         this.port = (port == undefined) ? location.port : port;
         this.rpc_mode = (server == location.hostname) ? "ajax" : "jsonp";
         this.debug = (window.location.search.indexOf('?debug') !== -1);
-        this.db = "";
-        this.login = "";
-        this.password = "";
-        this.user_context= {};
-        this.uid = false;
         this.session_id = false;
+        this.uid = false;
+        this.user_context= {};
+        this.db = false;
         this.module_list = [];
         this.module_loaded = {"web": true};
         this.context = {};
         this.shortcuts = [];
         this.active_id = null;
-        this.session = this;
     },
     start: function() {
         this.session_restore();
@@ -375,7 +394,7 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
             jsonrpc: "2.0",
             method: "call",
             params: params,
-            id:null
+            id: _.uniqueId('browser-client-')
         }).then(function () {deferred.resolve.apply(deferred, arguments);},
                 function(error) {deferred.reject(error, $.Event());});
         return deferred.fail(function() {
@@ -461,56 +480,46 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
     },
     session_login: function(db, login, password, success_callback) {
         var self = this;
-        this.db = db;
-        this.login = login;
-        this.password = password;
-        var params = { db: this.db, login: this.login, password: this.password };
-        this.rpc("/web/session/login", params, function(result) {
+        var params = { db: db, login: login, password: password };
+        return this.rpc("/web/session/login", params, function(result) {
             self.session_id = result.session_id;
             self.uid = result.uid;
             self.user_context = result.context;
+            self.db = result.db;
             self.session_save();
-            self.on_session_valid();
-            if (success_callback)
-                success_callback();
-        });
-    },
-    session_logout: function() {
-        this.uid = false;
+            return true;
+        }).then(success_callback);
     },
     /**
      * Reloads uid and session_id from local storage, if they exist
      */
     session_restore: function () {
-        this.uid = this.get_cookie('uid');
+        var self = this;
         this.session_id = this.get_cookie('session_id');
-        this.db = this.get_cookie('db');
-        this.login = this.get_cookie('login');
-        this.user_context = this.get_cookie("user_context");
-        // we should do an rpc to confirm that this session_id is valid and if it is retrieve the information about db and login
-        // then call on_session_valid
-        this.on_session_valid();
+        return this.rpc("/web/session/get_session_info", {}).then(function(result) {
+            self.uid = result.uid;
+            self.user_context = result.context;
+            self.db = result.db;
+            if (self.uid)
+                self.on_session_valid();
+            else
+                self.on_session_invalid();
+        }, function() {
+            self.on_session_invalid();
+        });
     },
     /**
      * Saves the session id and uid locally
      */
     session_save: function () {
-        this.set_cookie('uid', this.uid);
         this.set_cookie('session_id', this.session_id);
-        this.set_cookie('db', this.db);
-        this.set_cookie('login', this.login);
-        this.set_cookie('user_context', this.user_context);
     },
     logout: function() {
-        delete this.uid;
-        delete this.session_id;
-        delete this.db;
-        delete this.login;
-        this.set_cookie('uid', '');
         this.set_cookie('session_id', '');
-        this.set_cookie('db', '');
-        this.set_cookie('login', '');
-        this.on_session_invalid(function() {});
+        this.reload_client();
+    },
+    reload_client: function() {
+        window.location.reload();
     },
     /**
      * Fetches a cookie stored by an openerp session
@@ -558,9 +567,8 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
                 openerp.web._t.database.set_bundle(transs);
                 var modules = self.module_list.join(',');
                 var file_list = ["/web/static/lib/datejs/globalization/" +
-                    self.user_context.lang.replace("_", "-") + ".js",
-
-                    ];
+                    self.user_context.lang.replace("_", "-") + ".js"
+                ];
                 if(self.debug) {
                     self.rpc('/web/webclient/csslist', {"mods": modules}, self.do_load_css);
                     self.rpc('/web/webclient/jslist', {"mods": modules}, function(files) {
@@ -575,9 +583,10 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
         });
     },
     do_load_css: function (files) {
+        var self = this;
         _.each(files, function (file) {
             $('head').append($('<link>', {
-                'href': file,
+                'href': file + (self.debug ? '?debug=' + (new Date().getTime()) : ''),
                 'rel': 'stylesheet',
                 'type': 'text/css'
             }));
@@ -589,14 +598,15 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
             var file = files.shift();
             var tag = document.createElement('script');
             tag.type = 'text/javascript';
-            tag.src = file;
+            tag.src = file + (this.debug ? '?debug=' + (new Date().getTime()) : '');
             tag.onload = tag.onreadystatechange = function() {
                 if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done )
                     return;
                 tag.onload_done = true;
                 self.do_load_js(files);
             };
-            document.head.appendChild(tag);
+            $('head').append(tag);
+            self.do_load_js(files);
         } else {
             this.on_modules_loaded();
         }
@@ -707,13 +717,18 @@ openerp.web.Session = openerp.web.CallbackEnabled.extend( /** @lends openerp.web
     }
 });
 
-/**
- * Utility class that any class is allowed to extend to easy common manipulations.
- *
- * It provides rpc calls, callback on all methods preceded by "on_" or "do_" and a
- * logging facility.
- */
-openerp.web.SessionAware = openerp.web.CallbackEnabled.extend({
+openerp.web.SessionAware = openerp.web.CallbackEnabled.extend(/** @lends openerp.web.SessionAware# */{
+    /**
+     * Utility class that any class is allowed to extend to easy common manipulations.
+     *
+     * It provides rpc calls, callback on all methods preceded by "on_" or "do_" and a
+     * logging facility.
+     *
+     * @constructs openerp.web.SessionAware
+     * @extends openerp.web.CallbackEnabled
+     *
+     * @param {openerp.web.Session} session
+     */
     init: function(session) {
         this._super();
         this.session = session;
@@ -729,43 +744,62 @@ openerp.web.SessionAware = openerp.web.CallbackEnabled.extend({
      */
     rpc: function(url, data, success, error) {
         return this.session.rpc(url, data, success, error);
-    },
-    log: function() {
-        var args = Array.prototype.slice.call(arguments);
-        var caller = arguments.callee.caller;
-        // TODO add support for line number using
-        // https://github.com/emwendelin/javascript-stacktrace/blob/master/stacktrace.js
-        // args.unshift("" + caller.debug_name);
-        this.on_log.apply(this,args);
-    },
-    on_log: function() {
-        if(this.session.debug) {
-            var notify = false;
-            var body = false;
-            if(window.console) {
-                console.log(arguments);
-            } else {
-                body = true;
-            }
-            var a = Array.prototype.slice.call(arguments, 0);
-            for(var i = 0; i < a.length; i++) {
-                var v = a[i]==null ? "null" : a[i].toString();
-                if(i==0) {
-                    notify = v.match(/^not/);
-                    body = v.match(/^bod/);
-                }
-                if(body) {
-                    $('<pre></pre>').text(v).appendTo($('body'));
-                }
-                if(notify && this.notification) {
-                    this.notification.notify("Logging:",v);
-                }
-            }
-        }
     }
 });
 
-openerp.web.Widget = openerp.web.SessionAware.extend({
+/**
+ * Base class for all visual components. Provides a lot of functionalities helpful
+ * for the management of a part of the DOM.
+ *
+ * Widget handles:
+ * - Rendering with QWeb.
+ * - Life-cycle management and parenting (when a parent is destroyed, all its children are
+ *     destroyed too).
+ * - Insertion in DOM.
+ *
+ * Widget also extends SessionAware for ease of use.
+ *
+ * Guide to create implementations of the Widget class:
+ * ==============================================
+ *
+ * Here is a sample child class:
+ *
+ * MyWidget = openerp.base.Widget.extend({
+ *     // the name of the QWeb template to use for rendering
+ *     template: "MyQWebTemplate",
+ *     // identifier prefix, it is useful to put an obvious one for debugging
+ *     identifier_prefix: 'my-id-prefix-',
+ *
+ *     init: function(parent) {
+ *         this._super(parent);
+ *         // stuff that you want to init before the rendering
+ *     },
+ *     start: function() {
+ *         // stuff you want to make after the rendering, `this.$element` holds a correct value
+ *         this.$element.find(".my_button").click(/* an example of event binding * /);
+ *
+ *         // if you have some asynchronous operations, it's a good idea to return
+ *         // a promise in start()
+ *         var promise = this.rpc(...);
+ *         return promise;
+ *     }
+ * });
+ *
+ * Now this class can simply be used with the following syntax:
+ *
+ * var my_widget = new MyWidget(this);
+ * my_widget.appendTo($(".some-div"));
+ *
+ * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
+ * DOM inside the ".some-div" div and its events were binded.
+ *
+ * And of course, when you don't need that widget anymore, just do:
+ *
+ * my_widget.stop();
+ *
+ * That will kill the widget in a clean way and erase its content from the dom.
+ */
+openerp.web.Widget = openerp.web.SessionAware.extend(/** @lends openerp.web.Widget# */{
     /**
      * The name of the QWeb template that will be used for rendering. Must be
      * redefined in subclasses or the default render() method can not be used.
@@ -783,7 +817,9 @@ openerp.web.Widget = openerp.web.SessionAware.extend({
     /**
      * Construct the widget and set its parent if a parent is given.
      *
-     * @constructs
+     * @constructs openerp.web.Widget
+     * @extends openerp.web.SessionAware
+     *
      * @param {openerp.web.Widget} parent Binds the current instance to the given Widget instance.
      * When that widget is destroyed by calling stop(), the current instance will be
      * destroyed too. Can be null.
@@ -859,8 +895,10 @@ openerp.web.Widget = openerp.web.SessionAware.extend({
         if (target instanceof openerp.web.Widget)
             target = target.$element;
         insertion(target);
+        this.on_inserted(this.$element, this);
         return this.start();
     },
+    on_inserted: function(element, widget) {},
     /**
      * Renders the widget using QWeb, `this.template` must be defined.
      * The context given to QWeb contains the "widget" key that references `this`.
@@ -868,7 +906,7 @@ openerp.web.Widget = openerp.web.SessionAware.extend({
      * @param {Object} additional Additional context arguments to pass to the template.
      */
     render: function (additional) {
-        return QWeb.render(this.template, _.extend({widget: this}, additional || {}));
+        return openerp.web.qweb.render(this.template, _.extend({widget: this}, additional || {}));
     },
     /**
      * Method called after rendering. Mostly used to bind actions, perform asynchronous
@@ -880,6 +918,8 @@ openerp.web.Widget = openerp.web.SessionAware.extend({
      * @returns {jQuery.Deferred}
      */
     start: function() {
+        /* The default implementation is only useful for retro-compatibility, it is
+        not necessary to call it using _super() when using Widget for new components. */
         if (!this.$element) {
             var tmp = document.getElementById(this.element_id);
             this.$element = tmp ? $(tmp) : undefined;
@@ -887,7 +927,7 @@ openerp.web.Widget = openerp.web.SessionAware.extend({
         return $.Deferred().done().promise();
     },
     /**
-     * Destroys the current widget, also destory all its children before destroying itself.
+     * Destroys the current widget, also destroy all its children before destroying itself.
      */
     stop: function() {
         _.each(_.clone(this.widget_children), function(el) {
@@ -940,17 +980,23 @@ openerp.web.Widget = openerp.web.SessionAware.extend({
 });
 
 /**
+ * @class
+ * @extends openerp.web.Widget
  * @deprecated
  * For retro compatibility only, the only difference with is that render() uses
- * directly `this` instead of context with a "widget" key.
+ * directly ``this`` instead of context with a ``widget`` key.
  */
-openerp.web.OldWidget = openerp.web.Widget.extend({
+openerp.web.OldWidget = openerp.web.Widget.extend(/** @lends openerp.web.OldWidget# */{
     render: function (additional) {
-        return QWeb.render(this.template, _.extend(_.extend({}, this), additional || {}));
+        return openerp.web.qweb.render(this.template, _.extend(_.extend({}, this), additional || {}));
     }
 });
 
-openerp.web.TranslationDataBase = openerp.web.Class.extend({
+openerp.web.TranslationDataBase = openerp.web.Class.extend(/** @lends openerp.web.TranslationDataBase# */{
+    /**
+     * @constructs openerp.web.TranslationDataBase
+     * @extends openerp.web.Class
+     */
     init: function() {
         this.db = {};
         this.parameters = {"direction": 'ltr',