[MERGE]merge with main branch.
[odoo/odoo.git] / addons / web / static / src / js / core.js
1 /*---------------------------------------------------------
2  * OpenERP Web core
3  *--------------------------------------------------------*/
4 var console;
5 if (!console) {
6     console = {log: function () {}};
7 }
8 if (!console.debug) {
9     console.debug = console.log;
10 }
11
12 openerp.web.core = function(openerp) {
13 /**
14  * John Resig Class with factory improvement
15  */
16 (function() {
17     var initializing = false,
18         fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
19     // The web Class implementation (does nothing)
20     /**
21      * Extended version of John Resig's Class pattern
22      *
23      * @class
24      */
25     openerp.web.Class = function(){};
26
27     /**
28      * Subclass an existing class
29      *
30      * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
31      */
32     openerp.web.Class.extend = function(prop) {
33         var _super = this.prototype;
34
35         // Instantiate a web class (but only create the instance,
36         // don't run the init constructor)
37         initializing = true;
38         var prototype = new this();
39         initializing = false;
40
41         // Copy the properties over onto the new prototype
42         for (var name in prop) {
43             // Check if we're overwriting an existing function
44             prototype[name] = typeof prop[name] == "function" &&
45                               typeof _super[name] == "function" &&
46                               fnTest.test(prop[name]) ?
47                     (function(name, fn) {
48                         return function() {
49                             var tmp = this._super;
50
51                             // Add a new ._super() method that is the same
52                             // method but on the super-class
53                             this._super = _super[name];
54
55                             // The method only need to be bound temporarily, so
56                             // we remove it when we're done executing
57                             var ret = fn.apply(this, arguments);
58                             this._super = tmp;
59
60                             return ret;
61                         };
62                     })(name, prop[name]) :
63                     prop[name];
64         }
65
66         // The dummy class constructor
67         function Class() {
68             // All construction is actually done in the init method
69             if (!initializing && this.init) {
70                 var ret = this.init.apply(this, arguments);
71                 if (ret) { return ret; }
72             }
73             return this;
74         }
75         Class.include = function (properties) {
76             for (var name in properties) {
77                 if (typeof properties[name] !== 'function'
78                         || !fnTest.test(properties[name])) {
79                     prototype[name] = properties[name];
80                 } else if (typeof prototype[name] === 'function'
81                            && prototype.hasOwnProperty(name)) {
82                     prototype[name] = (function (name, fn, previous) {
83                         return function () {
84                             var tmp = this._super;
85                             this._super = previous;
86                             var ret = fn.apply(this, arguments);
87                             this._super = tmp;
88                             return ret;
89                         }
90                     })(name, properties[name], prototype[name]);
91                 } else if (typeof _super[name] === 'function') {
92                     prototype[name] = (function (name, fn) {
93                         return function () {
94                             var tmp = this._super;
95                             this._super = _super[name];
96                             var ret = fn.apply(this, arguments);
97                             this._super = tmp;
98                             return ret;
99                         }
100                     })(name, properties[name]);
101                 }
102             }
103         };
104
105         // Populate our constructed prototype object
106         Class.prototype = prototype;
107
108         // Enforce the constructor to be what we expect
109         Class.constructor = Class;
110
111         // And make this class extendable
112         Class.extend = arguments.callee;
113
114         return Class;
115     };
116 })();
117
118 openerp.web.callback = function(obj, method) {
119     var callback = function() {
120         var args = Array.prototype.slice.call(arguments);
121         var r;
122         for(var i = 0; i < callback.callback_chain.length; i++)  {
123             var c = callback.callback_chain[i];
124             if(c.unique) {
125                 callback.callback_chain.splice(i, 1);
126                 i -= 1;
127             }
128             var result = c.callback.apply(c.self, c.args.concat(args));
129             if (c.callback === method) {
130                 // return the result of the original method
131                 r = result;
132             }
133             // TODO special value to stop the chain
134             // openerp.web.callback_stop
135         }
136         return r;
137     };
138     callback.callback_chain = [];
139     callback.add = function(f) {
140         if(typeof(f) == 'function') {
141             f = { callback: f, args: Array.prototype.slice.call(arguments, 1) };
142         }
143         f.self = f.self || null;
144         f.args = f.args || [];
145         f.unique = !!f.unique;
146         if(f.position == 'last') {
147             callback.callback_chain.push(f);
148         } else {
149             callback.callback_chain.unshift(f);
150         }
151         return callback;
152     };
153     callback.add_first = function(f) {
154         return callback.add.apply(null,arguments);
155     };
156     callback.add_last = function(f) {
157         return callback.add({
158             callback: f,
159             args: Array.prototype.slice.call(arguments, 1),
160             position: "last"
161         });
162     };
163     callback.remove = function(f) {
164         callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) {
165             return el.callback === f;
166         }));
167         return callback;
168     };
169
170     return callback.add({
171         callback: method,
172         self:obj,
173         args:Array.prototype.slice.call(arguments, 2)
174     });
175 };
176
177 /**
178  * Generates an inherited class that replaces all the methods by null methods (methods
179  * that does nothing and always return undefined).
180  *
181  * @param {Class} claz
182  * @param {Object} add Additional functions to override.
183  * @return {Class}
184  */
185 openerp.web.generate_null_object_class = function(claz, add) {
186     var newer = {};
187     var copy_proto = function(prototype) {
188         for (var name in prototype) {
189             if(typeof prototype[name] == "function") {
190                 newer[name] = function() {};
191             }
192         }
193         if (prototype.prototype)
194             copy_proto(prototype.prototype);
195     };
196     copy_proto(claz.prototype);
197     newer.init = openerp.web.Widget.prototype.init;
198     var tmpclass = claz.extend(newer);
199     return tmpclass.extend(add || {});
200 };
201
202 /**
203  * web error for lookup failure
204  *
205  * @class
206  */
207 openerp.web.NotFound = openerp.web.Class.extend( /** @lends openerp.web.NotFound# */ {
208 });
209 openerp.web.KeyNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.KeyNotFound# */ {
210     /**
211      * Thrown when a key could not be found in a mapping
212      *
213      * @constructs openerp.web.KeyNotFound
214      * @extends openerp.web.NotFound
215      * @param {String} key the key which could not be found
216      */
217     init: function (key) {
218         this.key = key;
219     },
220     toString: function () {
221         return "The key " + this.key + " was not found";
222     }
223 });
224 openerp.web.ObjectNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.ObjectNotFound# */ {
225     /**
226      * Thrown when an object path does not designate a valid class or object
227      * in the openerp hierarchy.
228      *
229      * @constructs openerp.web.ObjectNotFound
230      * @extends openerp.web.NotFound
231      * @param {String} path the invalid object path
232      */
233     init: function (path) {
234         this.path = path;
235     },
236     toString: function () {
237         return "Could not find any object of path " + this.path;
238     }
239 });
240 openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry# */ {
241     /**
242      * Stores a mapping of arbitrary key (strings) to object paths (as strings
243      * as well).
244      *
245      * Resolves those paths at query time in order to always fetch the correct
246      * object, even if those objects have been overloaded/replaced after the
247      * registry was created.
248      *
249      * An object path is simply a dotted name from the openerp root to the
250      * object pointed to (e.g. ``"openerp.web.Connection"`` for an OpenERP
251      * connection object).
252      *
253      * @constructs openerp.web.Registry
254      * @param {Object} mapping a mapping of keys to object-paths
255      */
256     init: function (mapping) {
257         this.map = mapping || {};
258     },
259     /**
260      * Retrieves the object matching the provided key string.
261      *
262      * @param {String} key the key to fetch the object for
263      * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception
264      * @returns {Class} the stored class, to initialize
265      *
266      * @throws {openerp.web.KeyNotFound} if the object was not in the mapping
267      * @throws {openerp.web.ObjectNotFound} if the object path was invalid
268      */
269     get_object: function (key, silent_error) {
270         var path_string = this.map[key];
271         if (path_string === undefined) {
272             if (silent_error) { return void 'nooo'; }
273             throw new openerp.web.KeyNotFound(key);
274         }
275
276         var object_match = openerp;
277         var path = path_string.split('.');
278         // ignore first section
279         for(var i=1; i<path.length; ++i) {
280             object_match = object_match[path[i]];
281
282             if (object_match === undefined) {
283                 if (silent_error) { return void 'noooooo'; }
284                 throw new openerp.web.ObjectNotFound(path_string);
285             }
286         }
287         return object_match;
288     },
289     /**
290      * Tries a number of keys, and returns the first object matching one of
291      * the keys.
292      *
293      * @param {Array} keys a sequence of keys to fetch the object for
294      * @returns {Class} the first class found matching an object
295      *
296      * @throws {openerp.web.KeyNotFound} if none of the keys was in the mapping
297      * @trows {openerp.web.ObjectNotFound} if a found object path was invalid
298      */
299     get_any: function (keys) {
300         for (var i=0; i<keys.length; ++i) {
301             var key = keys[i];
302             if (key === undefined || !(key in this.map)) {
303                 continue;
304             }
305
306             return this.get_object(key);
307         }
308         throw new openerp.web.KeyNotFound(keys.join(','));
309     },
310     /**
311      * Adds a new key and value to the registry.
312      *
313      * This method can be chained.
314      *
315      * @param {String} key
316      * @param {String} object_path fully qualified dotted object path
317      * @returns {openerp.web.Registry} itself
318      */
319     add: function (key, object_path) {
320         this.map[key] = object_path;
321         return this;
322     },
323     /**
324      * Creates and returns a copy of the current mapping, with the provided
325      * mapping argument added in (replacing existing keys if needed)
326      *
327      * @param {Object} [mapping={}] a mapping of keys to object-paths
328      */
329     clone: function (mapping) {
330         return new openerp.web.Registry(
331             _.extend({}, this.map, mapping || {}));
332     }
333 });
334
335 openerp.web.CallbackEnabled = openerp.web.Class.extend(/** @lends openerp.web.CallbackEnabled# */{
336     /**
337      * @constructs openerp.web.CallbackEnabled
338      * @extends openerp.web.Class
339      */
340     init: function() {
341         // Transform on_* method into openerp.web.callbacks
342         for (var name in this) {
343             if(typeof(this[name]) == "function") {
344                 this[name].debug_name = name;
345                 // bind ALL function to this not only on_and _do ?
346                 if((/^on_|^do_/).test(name)) {
347                     this[name] = openerp.web.callback(this, this[name]);
348                 }
349             }
350         }
351     }
352 });
353
354 openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp.web.Connection# */{
355     /**
356      * @constructs openerp.web.Connection
357      * @extends openerp.web.CallbackEnabled
358      *
359      * @param {String} [server] JSON-RPC endpoint hostname
360      * @param {String} [port] JSON-RPC endpoint port
361      */
362     init: function() {
363         this._super();
364         this.server = null;
365         this.debug = ($.deparam($.param.querystring()).debug != undefined);
366         // TODO: session store in cookie should be optional
367         this.name = openerp._session_id;
368         this.qweb_mutex = new $.Mutex();
369     },
370     bind: function(origin) {
371         var window_origin = location.protocol+"//"+location.host;
372         this.origin = origin ? _.str.rtrim(origin,'/') : window_origin;
373         this.prefix = this.origin;
374         this.server = this.origin; // keep chs happy
375         openerp.web.qweb.default_dict['_s'] = this.origin;
376         this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp;
377         this.session_id = false;
378         this.uid = false;
379         this.username = false;
380         this.user_context= {};
381         this.db = false;
382         this.openerp_entreprise = false;
383         this.module_list = [];
384         this.module_loaded = {"web": true};
385         this.context = {};
386         this.shortcuts = [];
387         this.active_id = null;
388         return this.session_init();
389     },
390     /**
391      * Executes an RPC call, registering the provided callbacks.
392      *
393      * Registers a default error callback if none is provided, and handles
394      * setting the correct session id and session context in the parameter
395      * objects
396      *
397      * @param {String} url RPC endpoint
398      * @param {Object} params call parameters
399      * @param {Function} success_callback function to execute on RPC call success
400      * @param {Function} error_callback function to execute on RPC call failure
401      * @returns {jQuery.Deferred} jquery-provided ajax deferred
402      */
403     rpc: function(url, params, success_callback, error_callback) {
404         var self = this;
405         // url can be an $.ajax option object
406         if (_.isString(url)) {
407             url = { url: url };
408         }
409         // Construct a JSON-RPC2 request, method is currently unused
410         params.session_id = this.session_id;
411         if (this.debug)
412             params.debug = 1;
413         var payload = {
414             jsonrpc: '2.0',
415             method: 'call',
416             params: params,
417             id: _.uniqueId('r')
418         };
419         var deferred = $.Deferred();
420         this.on_rpc_request();
421         this.rpc_function(url, payload).then(
422             function (response, textStatus, jqXHR) {
423                 self.on_rpc_response();
424                 if (!response.error) {
425                     deferred.resolve(response["result"], textStatus, jqXHR);
426                 } else if (response.error.data.type === "session_invalid") {
427                     self.uid = false;
428                     // TODO deprecate or use a deferred on login.do_ask_login()
429                     self.on_session_invalid(function() {
430                         self.rpc(url, payload.params,
431                             function() { deferred.resolve.apply(deferred, arguments); },
432                             function() { deferred.reject.apply(deferred, arguments); });
433                     });
434                 } else {
435                     deferred.reject(response.error, $.Event());
436                 }
437             },
438             function(jqXHR, textStatus, errorThrown) {
439                 self.on_rpc_response();
440                 var error = {
441                     code: -32098,
442                     message: "XmlHttpRequestError " + errorThrown,
443                     data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] }
444                 };
445                 deferred.reject(error, $.Event());
446             });
447         // Allow deferred user to disable on_rpc_error in fail
448         deferred.fail(function() {
449             deferred.fail(function(error, event) {
450                 if (!event.isDefaultPrevented()) {
451                     self.on_rpc_error(error, event);
452                 }
453             });
454         }).then(success_callback, error_callback).promise();
455         return deferred;
456     },
457     /**
458      * Raw JSON-RPC call
459      *
460      * @returns {jQuery.Deferred} ajax-webd deferred object
461      */
462     rpc_json: function(url, payload) {
463         var self = this;
464         var ajax = _.extend({
465             type: "POST",
466             dataType: 'json',
467             contentType: 'application/json',
468             data: JSON.stringify(payload),
469             processData: false,
470         }, url);
471         if (this.synch)
472                 ajax.async = false;
473         return $.ajax(ajax);
474     },
475     rpc_jsonp: function(url, payload) {
476         var self = this;
477         // extracted from payload to set on the url
478         var data = {
479             session_id: this.session_id,
480             id: payload.id,
481         };
482         url.url = this.get_url(url.url);
483         var ajax = _.extend({
484             type: "GET",
485             dataType: 'jsonp', 
486             jsonp: 'jsonp',
487             cache: false,
488             data: data
489         }, url);
490         if (this.synch)
491                 ajax.async = false;
492         var payload_str = JSON.stringify(payload);
493         var payload_url = $.param({r:payload_str});
494         if(payload_url.length < 2000) {
495             // Direct jsonp request
496             ajax.data.r = payload_str;
497             return $.ajax(ajax);
498         } else {
499             // Indirect jsonp request
500             var ifid = _.uniqueId('oe_rpc_iframe');
501             var display = options.openerp.debug ? 'block' : 'none';
502             var $iframe = $(_.str.sprintf("<iframe src='javascript:false;' name='%s' id='%s' style='display:%s'></iframe>", ifid, ifid, display));
503             var $form = $('<form>')
504                         .attr('method', 'POST')
505                         .attr('target', ifid)
506                         .attr('enctype', "multipart/form-data")
507                         .attr('action', ajax.url + '?' + $.param(data))
508                         .append($('<input type="hidden" name="r" />').attr('value', payload_str))
509                         .hide()
510                         .appendTo($('body'));
511             var cleanUp = function() {
512                 if ($iframe) {
513                     $iframe.unbind("load").attr("src", "javascript:false;").remove();
514                 }
515                 $form.remove();
516             };
517             var deferred = $.Deferred();
518             // the first bind is fired up when the iframe is added to the DOM
519             $iframe.bind('load', function() {
520                 // the second bind is fired up when the result of the form submission is received
521                 $iframe.unbind('load').bind('load', function() {
522                     $.ajax(ajax).always(function() {
523                         cleanUp();
524                     }).then(
525                         function() { deferred.resolve.apply(deferred, arguments); },
526                         function() { deferred.reject.apply(deferred, arguments); }
527                     );
528                 });
529                 // now that the iframe can receive data, we fill and submit the form
530                 $form.submit();
531             });
532             // append the iframe to the DOM (will trigger the first load)
533             $form.after($iframe);
534             return deferred;
535         }
536     },
537     on_rpc_request: function() {
538     },
539     on_rpc_response: function() {
540     },
541     on_rpc_error: function(error) {
542     },
543     /**
544      * Init a session, reloads from cookie, if it exists
545      */
546     session_init: function () {
547         var self = this;
548         // TODO: session store in cookie should be optional
549         this.session_id = this.get_cookie('session_id');
550         return this.rpc("/web/session/get_session_info", {}).pipe(function(result) {
551             // If immediately follows a login (triggered by trying to restore
552             // an invalid session or no session at all), refresh session data
553             // (should not change, but just in case...)
554             _.extend(self, {
555                 db: result.db,
556                 username: result.login,
557                 uid: result.uid,
558                 user_context: result.context,
559                 openerp_entreprise: result.openerp_entreprise
560             });
561             var modules = openerp._modules.join(',');
562             var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).pipe(self.do_load_qweb);
563             if(self.session_is_valid()) {
564                 return deferred.pipe(function() { self.load_modules(); });
565             }
566             return deferred;
567         });
568     },
569     session_is_valid: function() {
570         return !!this.uid;
571     },
572     /**
573      * The session is validated either by login or by restoration of a previous session
574      */
575     session_authenticate: function(db, login, password, _volatile) {
576         var self = this;
577         var base_location = document.location.protocol + '//' + document.location.host;
578         var params = { db: db, login: login, password: password, base_location: base_location };
579         return this.rpc("/web/session/authenticate", params).pipe(function(result) {
580             _.extend(self, {
581                 session_id: result.session_id,
582                 db: result.db,
583                 username: result.login,
584                 uid: result.uid,
585                 user_context: result.context,
586                 openerp_entreprise: result.openerp_entreprise
587             });
588             if (!_volatile) {
589                 self.set_cookie('session_id', self.session_id);
590             }
591             return self.load_modules();
592         });
593     },
594     session_logout: function() {
595         this.set_cookie('session_id', '');
596     },
597     on_session_valid: function() {
598     },
599     /**
600      * Called when a rpc call fail due to an invalid session.
601      * By default, it's a noop
602      */
603     on_session_invalid: function(retry_callback) {
604     },
605     /**
606      * Fetches a cookie stored by an openerp session
607      *
608      * @private
609      * @param name the cookie's name
610      */
611     get_cookie: function (name) {
612         if (!this.name) { return null; }
613         var nameEQ = this.name + '|' + name + '=';
614         var cookies = document.cookie.split(';');
615         for(var i=0; i<cookies.length; ++i) {
616             var cookie = cookies[i].replace(/^\s*/, '');
617             if(cookie.indexOf(nameEQ) === 0) {
618                 return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
619             }
620         }
621         return null;
622     },
623     /**
624      * Create a new cookie with the provided name and value
625      *
626      * @private
627      * @param name the cookie's name
628      * @param value the cookie's value
629      * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete
630      */
631     set_cookie: function (name, value, ttl) {
632         if (!this.name) { return; }
633         ttl = ttl || 24*60*60*365;
634         document.cookie = [
635             this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
636             'path=/',
637             'max-age=' + ttl,
638             'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
639         ].join(';');
640     },
641     /**
642      * Load additional web addons of that instance and init them
643      */
644     load_modules: function() {
645         var self = this;
646         return this.rpc('/web/session/modules', {}).pipe(function(result) {
647             self.module_list = result;
648             var lang = self.user_context.lang;
649             var params = { mods: ["web"].concat(result), lang: lang};
650             var modules = self.module_list.join(',');
651             return $.when(
652                 self.rpc('/web/webclient/csslist', {mods: modules}, self.do_load_css),
653                 self.rpc('/web/webclient/qweblist', {mods: modules}).pipe(self.do_load_qweb),
654                 self.rpc('/web/webclient/translations', params).pipe(function(trans) {
655                     openerp.web._t.database.set_bundle(trans);
656                     var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"];
657                     return self.rpc('/web/webclient/jslist', {mods: modules}).pipe(function(files) {
658                         return self.do_load_js(file_list.concat(files)); 
659                     });
660                 })
661             ).then(function() {
662                 self.on_session_valid();
663             });
664         });
665     },
666     do_load_css: function (files) {
667         var self = this;
668         _.each(files, function (file) {
669             $('head').append($('<link>', {
670                 'href': self.get_url(file),
671                 'rel': 'stylesheet',
672                 'type': 'text/css'
673             }));
674         });
675     },
676     do_load_js: function(files) {
677         var self = this;
678         var d = $.Deferred();
679         if(files.length != 0) {
680             var file = files.shift();
681             var tag = document.createElement('script');
682             tag.type = 'text/javascript';
683             tag.src = self.get_url(file);
684             tag.onload = tag.onreadystatechange = function() {
685                 if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done )
686                     return;
687                 tag.onload_done = true;
688                 self.do_load_js(files).then(function () {
689                     d.resolve();
690                 });
691             };
692             var head = document.head || document.getElementsByTagName('head')[0];
693             head.appendChild(tag);
694         } else {
695             self.on_modules_loaded();
696             d.resolve();
697         }
698         return d;
699     },
700     do_load_qweb: function(files) {
701         var self = this;
702         _.each(files, function(file) {
703             self.qweb_mutex.exec(function() {
704                 return self.rpc('/web/proxy/load', {path: file}).pipe(function(xml) {
705                     openerp.web.qweb.add_template(_.str.trim(xml));
706                 });
707             });
708         });
709         return self.qweb_mutex.def;
710     },
711     on_modules_loaded: function() {
712         for(var j=0; j<this.module_list.length; j++) {
713             var mod = this.module_list[j];
714             if(this.module_loaded[mod])
715                 continue;
716             openerp[mod] = {};
717             // init module mod
718             if(openerp._openerp[mod] != undefined) {
719                 openerp._openerp[mod](openerp);
720                 this.module_loaded[mod] = true;
721             }
722         }
723     },
724     get_url: function (file) {
725         return this.prefix + file;
726     },
727     /**
728      * Cooperative file download implementation, for ajaxy APIs.
729      *
730      * Requires that the server side implements an httprequest correctly
731      * setting the `fileToken` cookie to the value provided as the `token`
732      * parameter. The cookie *must* be set on the `/` path and *must not* be
733      * `httpOnly`.
734      *
735      * It would probably also be a good idea for the response to use a
736      * `Content-Disposition: attachment` header, especially if the MIME is a
737      * "known" type (e.g. text/plain, or for some browsers application/json
738      *
739      * @param {Object} options
740      * @param {String} [options.url] used to dynamically create a form
741      * @param {Object} [options.data] data to add to the form submission. If can be used without a form, in which case a form is created from scratch. Otherwise, added to form data
742      * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file
743      * @param {Function} [options.success] callback in case of download success
744      * @param {Function} [options.error] callback in case of request error, provided with the error body
745      * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed
746      */
747     get_file: function (options) {
748         // need to detect when the file is done downloading (not used
749         // yet, but we'll need it to fix the UI e.g. with a throbber
750         // while dump is being generated), iframe load event only fires
751         // when the iframe content loads, so we need to go smarter:
752         // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx
753         var timer, token = new Date().getTime(),
754             cookie_name = 'fileToken', cookie_length = cookie_name.length,
755             CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'),
756             remove_form = false;
757
758         var $form, $form_data = $('<div>');
759
760         var complete = function () {
761             if (options.complete) { options.complete(); }
762             clearTimeout(timer);
763             $form_data.remove();
764             $target.remove();
765             if (remove_form && $form) { $form.remove(); }
766         };
767         var $target = $('<iframe style="display: none;">')
768             .attr({id: id, name: id})
769             .appendTo(document.body)
770             .load(function () {
771                 if (options.error) { options.error(this.contentDocument.body); }
772                 complete();
773             });
774
775         if (options.form) {
776             $form = $(options.form);
777         } else {
778             remove_form = true;
779             $form = $('<form>', {
780                 action: options.url,
781                 method: 'POST'
782             }).appendTo(document.body);
783         }
784
785         _(_.extend({}, options.data || {},
786                    {session_id: this.session_id, token: token}))
787             .each(function (value, key) {
788                 $('<input type="hidden" name="' + key + '">')
789                     .val(value)
790                     .appendTo($form_data);
791             });
792
793         $form
794             .append($form_data)
795             .attr('target', id)
796             .get(0).submit();
797
798         var waitLoop = function () {
799             var cookies = document.cookie.split(';');
800             // setup next check
801             timer = setTimeout(waitLoop, CHECK_INTERVAL);
802             for (var i=0; i<cookies.length; ++i) {
803                 var cookie = cookies[i].replace(/^\s*/, '');
804                 if (!cookie.indexOf(cookie_name === 0)) { continue; }
805                 var cookie_val = cookie.substring(cookie_length + 1);
806                 if (parseInt(cookie_val, 10) !== token) { continue; }
807
808                 // clear cookie
809                 document.cookie = _.str.sprintf("%s=;expires=%s;path=/",
810                     cookie_name, new Date().toGMTString());
811                 if (options.success) { options.success(); }
812                 complete();
813                 return;
814             }
815         };
816         timer = setTimeout(waitLoop, CHECK_INTERVAL);
817     },
818     synchronized_mode: function(to_execute) {
819         var synch = this.synch;
820         this.synch = true;
821         try {
822                 return to_execute();
823         } finally {
824                 this.synch = synch;
825         }
826     },
827 });
828
829 /**
830  * Base class for all visual components. Provides a lot of functionalities helpful
831  * for the management of a part of the DOM.
832  *
833  * Widget handles:
834  * - Rendering with QWeb.
835  * - Life-cycle management and parenting (when a parent is destroyed, all its children are
836  *     destroyed too).
837  * - Insertion in DOM.
838  *
839  * Guide to create implementations of the Widget class:
840  * ==============================================
841  *
842  * Here is a sample child class:
843  *
844  * MyWidget = openerp.base.Widget.extend({
845  *     // the name of the QWeb template to use for rendering
846  *     template: "MyQWebTemplate",
847  *     // identifier prefix, it is useful to put an obvious one for debugging
848  *     identifier_prefix: 'my-id-prefix-',
849  *
850  *     init: function(parent) {
851  *         this._super(parent);
852  *         // stuff that you want to init before the rendering
853  *     },
854  *     start: function() {
855  *         // stuff you want to make after the rendering, `this.$element` holds a correct value
856  *         this.$element.find(".my_button").click(/* an example of event binding * /);
857  *
858  *         // if you have some asynchronous operations, it's a good idea to return
859  *         // a promise in start()
860  *         var promise = this.rpc(...);
861  *         return promise;
862  *     }
863  * });
864  *
865  * Now this class can simply be used with the following syntax:
866  *
867  * var my_widget = new MyWidget(this);
868  * my_widget.appendTo($(".some-div"));
869  *
870  * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
871  * DOM inside the ".some-div" div and its events were binded.
872  *
873  * And of course, when you don't need that widget anymore, just do:
874  *
875  * my_widget.stop();
876  *
877  * That will kill the widget in a clean way and erase its content from the dom.
878  */
879 openerp.web.Widget = openerp.web.CallbackEnabled.extend(/** @lends openerp.web.Widget# */{
880     /**
881      * The name of the QWeb template that will be used for rendering. Must be
882      * redefined in subclasses or the default render() method can not be used.
883      *
884      * @type string
885      */
886     template: null,
887     /**
888      * The prefix used to generate an id automatically. Should be redefined in
889      * subclasses. If it is not defined, a generic identifier will be used.
890      *
891      * @type string
892      */
893     identifier_prefix: 'generic-identifier-',
894     /**
895      * Tag name when creating a default $element.
896      * @type string
897      */
898     tag_name: 'div',
899     /**
900      * Constructs the widget and sets its parent if a parent is given.
901      *
902      * @constructs openerp.web.Widget
903      * @extends openerp.web.CallbackEnabled
904      *
905      * @param {openerp.web.Widget} parent Binds the current instance to the given Widget instance.
906      * When that widget is destroyed by calling stop(), the current instance will be
907      * destroyed too. Can be null.
908      * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want
909      * to bind the current Widget to an already existing part of the DOM, which is not compatible
910      * with the DOM insertion methods provided by the current implementation of Widget. So
911      * for new components this argument should not be provided any more.
912      */
913     init: function(parent, /** @deprecated */ element_id) {
914         this._super();
915         this.session = openerp.connection;
916         // if given an element_id, try to get the associated DOM element and save
917         // a reference in this.$element. Else just generate a unique identifier.
918         this.element_id = element_id;
919         this.element_id = this.element_id || _.uniqueId(this.identifier_prefix);
920         var tmp = document.getElementById(this.element_id);
921         this.$element = tmp ? $(tmp) : $(document.createElement(this.tag_name));
922
923         this.widget_parent = parent;
924         this.widget_children = [];
925         if(parent && parent.widget_children) {
926             parent.widget_children.push(this);
927         }
928         // useful to know if the widget was destroyed and should not be used anymore
929         this.widget_is_stopped = false;
930     },
931     /**
932      * Renders the current widget and appends it to the given jQuery object or Widget.
933      *
934      * @param target A jQuery object or a Widget instance.
935      */
936     appendTo: function(target) {
937         var self = this;
938         return this._render_and_insert(function(t) {
939             self.$element.appendTo(t);
940         }, target);
941     },
942     /**
943      * Renders the current widget and prepends it to the given jQuery object or Widget.
944      *
945      * @param target A jQuery object or a Widget instance.
946      */
947     prependTo: function(target) {
948         var self = this;
949         return this._render_and_insert(function(t) {
950             self.$element.prependTo(t);
951         }, target);
952     },
953     /**
954      * Renders the current widget and inserts it after to the given jQuery object or Widget.
955      *
956      * @param target A jQuery object or a Widget instance.
957      */
958     insertAfter: function(target) {
959         var self = this;
960         return this._render_and_insert(function(t) {
961             self.$element.insertAfter(t);
962         }, target);
963     },
964     /**
965      * Renders the current widget and inserts it before to the given jQuery object or Widget.
966      *
967      * @param target A jQuery object or a Widget instance.
968      */
969     insertBefore: function(target) {
970         var self = this;
971         return this._render_and_insert(function(t) {
972             self.$element.insertBefore(t);
973         }, target);
974     },
975     /**
976      * Renders the current widget and replaces the given jQuery object.
977      *
978      * @param target A jQuery object or a Widget instance.
979      */
980     replace: function(target) {
981         return this._render_and_insert(_.bind(function(t) {
982             this.$element.replaceAll(t);
983         }, this), target);
984     },
985     _render_and_insert: function(insertion, target) {
986         this.render_element();
987         if (target instanceof openerp.web.Widget)
988             target = target.$element;
989         insertion(target);
990         this.on_inserted(this.$element, this);
991         return this.start();
992     },
993     on_inserted: function(element, widget) {},
994     /**
995      * Renders the element and insert the result of the render() method in this.$element.
996      */
997     render_element: function() {
998         var rendered = this.render();
999         if (rendered) {
1000             var elem = $(rendered);
1001             this.$element.replaceWith(elem);
1002             this.$element = elem;
1003         }
1004         return this;
1005     },
1006     /**
1007      * Renders the widget using QWeb, `this.template` must be defined.
1008      * The context given to QWeb contains the "widget" key that references `this`.
1009      *
1010      * @param {Object} additional Additional context arguments to pass to the template.
1011      */
1012     render: function (additional) {
1013         if (this.template)
1014             return openerp.web.qweb.render(this.template, _.extend({widget: this}, additional || {}));
1015         return null;
1016     },
1017     /**
1018      * Method called after rendering. Mostly used to bind actions, perform asynchronous
1019      * calls, etc...
1020      *
1021      * By convention, the method should return a promise to inform the caller when
1022      * this widget has been initialized.
1023      *
1024      * @returns {jQuery.Deferred}
1025      */
1026     start: function() {
1027         return $.Deferred().done().promise();
1028     },
1029     /**
1030      * Destroys the current widget, also destroys all its children before destroying itself.
1031      */
1032     stop: function() {
1033         _.each(_.clone(this.widget_children), function(el) {
1034             el.stop();
1035         });
1036         if(this.$element != null) {
1037             this.$element.remove();
1038         }
1039         if (this.widget_parent && this.widget_parent.widget_children) {
1040             this.widget_parent.widget_children = _.without(this.widget_parent.widget_children, this);
1041         }
1042         this.widget_parent = null;
1043         this.widget_is_stopped = true;
1044     },
1045     /**
1046      * Informs the action manager to do an action. This supposes that
1047      * the action manager can be found amongst the ancestors of the current widget.
1048      * If that's not the case this method will simply return `false`.
1049      */
1050     do_action: function(action, on_finished) {
1051         if (this.widget_parent) {
1052             return this.widget_parent.do_action(action, on_finished);
1053         }
1054         return false;
1055     },
1056     do_notify: function() {
1057         if (this.widget_parent) {
1058             return this.widget_parent.do_notify.apply(this,arguments);
1059         }
1060         return false;
1061     },
1062     do_warn: function() {
1063         if (this.widget_parent) {
1064             return this.widget_parent.do_warn.apply(this,arguments);
1065         }
1066         return false;
1067     },
1068
1069     rpc: function(url, data, success, error) {
1070         var def = $.Deferred().then(success, error);
1071         var self = this;
1072         openerp.connection.rpc(url, data). then(function() {
1073             if (!self.widget_is_stopped)
1074                 def.resolve.apply(def, arguments);
1075         }, function() {
1076             if (!self.widget_is_stopped)
1077                 def.reject.apply(def, arguments);
1078         });
1079         return def.promise();
1080     }
1081 });
1082
1083 /**
1084  * @class
1085  * @extends openerp.web.Widget
1086  * @deprecated
1087  * For retro compatibility only, the only difference with is that render() uses
1088  * directly ``this`` instead of context with a ``widget`` key.
1089  */
1090 openerp.web.OldWidget = openerp.web.Widget.extend(/** @lends openerp.web.OldWidget# */{
1091     render: function (additional) {
1092         return openerp.web.qweb.render(this.template, _.extend(_.extend({}, this), additional || {}));
1093     }
1094 });
1095
1096 openerp.web.TranslationDataBase = openerp.web.Class.extend(/** @lends openerp.web.TranslationDataBase# */{
1097     /**
1098      * @constructs openerp.web.TranslationDataBase
1099      * @extends openerp.web.Class
1100      */
1101     init: function() {
1102         this.db = {};
1103         this.parameters = {"direction": 'ltr',
1104                         "date_format": '%m/%d/%Y',
1105                         "time_format": '%H:%M:%S',
1106                         "grouping": [],
1107                         "decimal_point": ".",
1108                         "thousands_sep": ","};
1109     },
1110     set_bundle: function(translation_bundle) {
1111         var self = this;
1112         this.db = {};
1113         var modules = _.keys(translation_bundle.modules);
1114         modules.sort();
1115         if (_.include(modules, "web")) {
1116             modules = ["web"].concat(_.without(modules, "web"));
1117         }
1118         _.each(modules, function(name) {
1119             self.add_module_translation(translation_bundle.modules[name]);
1120         });
1121         if (translation_bundle.lang_parameters) {
1122             this.parameters = translation_bundle.lang_parameters;
1123             this.parameters.grouping = py.eval(
1124                     this.parameters.grouping).toJSON();
1125         }
1126     },
1127     add_module_translation: function(mod) {
1128         var self = this;
1129         _.each(mod.messages, function(message) {
1130             if (self.db[message.id] === undefined) {
1131                 self.db[message.id] = message.string;
1132             }
1133         });
1134     },
1135     build_translation_function: function() {
1136         var self = this;
1137         var fcnt = function(str) {
1138             var tmp = self.get(str);
1139             return tmp === undefined ? str : tmp;
1140         };
1141         fcnt.database = this;
1142         return fcnt;
1143     },
1144     get: function(key) {
1145         if (this.db[key])
1146             return this.db[key];
1147         return undefined;
1148     }
1149 });
1150
1151 /** Configure blockui */
1152 if ($.blockUI) {
1153     $.blockUI.defaults.baseZ = 1100;
1154     $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">';
1155 }
1156
1157 /** Configure default qweb */
1158 openerp.web._t = new openerp.web.TranslationDataBase().build_translation_function();
1159 /**
1160  * Lazy translation function, only performs the translation when actually
1161  * printed (e.g. inserted into a template)
1162  *
1163  * Useful when defining translatable strings in code evaluated before the
1164  * translation database is loaded, as class attributes or at the top-level of
1165  * an OpenERP Web module
1166  *
1167  * @param {String} s string to translate
1168  * @returns {Object} lazy translation object
1169  */
1170 openerp.web._lt = function (s) {
1171     return {toString: function () { return openerp.web._t(s); }}
1172 };
1173 openerp.web.qweb = new QWeb2.Engine();
1174 openerp.web.qweb.debug = (window.location.search.indexOf('?debug') !== -1);
1175 openerp.web.qweb.default_dict = {
1176     '_' : _,
1177     '_t' : openerp.web._t
1178 };
1179 openerp.web.qweb.format_text_node = function(s) {
1180     // Note that 'this' is the Qweb Node of the text
1181     var translation = this.node.parentNode.attributes['t-translation'];
1182     if (translation && translation.value === 'off') {
1183         return s;
1184     }
1185     var ts = _.str.trim(s);
1186     if (ts.length === 0) {
1187         return s;
1188     }
1189     var tr = openerp.web._t(ts);
1190     return tr === ts ? s : tr;
1191 }
1192
1193 /** Jquery extentions */
1194 $.Mutex = (function() {
1195     function Mutex() {
1196         this.def = $.Deferred().resolve();
1197     };
1198     Mutex.prototype.exec = function(action) {
1199         var current = this.def;
1200         var next = this.def = $.Deferred();
1201         return current.pipe(function() {
1202             return $.when(action()).always(function() {
1203                 next.resolve();
1204             });
1205         });
1206     };
1207     return Mutex;
1208 })();
1209
1210 /** Setup default connection */
1211 openerp.connection = new openerp.web.Connection();
1212 openerp.web.qweb.default_dict['__debug__'] = openerp.connection.debug;
1213
1214
1215 $.async_when = function() {
1216     var async = false;
1217     var def = $.Deferred();
1218     $.when.apply($, arguments).then(function() {
1219         var args = arguments;
1220         var action = function() {
1221             def.resolve.apply(def, args);
1222         };
1223         if (async)
1224             action();
1225         else
1226             setTimeout(action, 0);
1227     }, function() {
1228         var args = arguments;
1229         var action = function() {
1230             def.reject.apply(def, args);
1231         };
1232         if (async)
1233             action();
1234         else
1235             setTimeout(action, 0);
1236     });
1237     async = true;
1238     return def;
1239 };
1240
1241 // special tweak for the web client
1242 var old_async_when = $.async_when;
1243 $.async_when = function() {
1244         if (openerp.connection.synch)
1245                 return $.when.apply(this, arguments);
1246         else
1247                 return old_async_when.apply(this, arguments);
1248 };
1249
1250 };
1251
1252 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: