[imp] get rid of include() in novajs
[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 // a function to override the "extend()" method of JR's inheritance, allowing
15 // the usage of "include()"
16 oe_override_class = function(claz){
17     var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ?
18     /\b_super\b/ : /.*/;
19     
20     // Create a new Class that inherits from this class
21     claz.extend = function(prop) {
22         var _super = this.prototype;
23         
24         // Instantiate a base class (but only create the instance, don't run the
25         // init constructor)
26         initializing = true; var prototype = new this(); initializing = false;
27         
28         // Copy the properties over onto the new prototype
29         for (var name in prop) {
30           // Check if we're overwriting an existing function
31           prototype[name] = typeof prop[name] == "function" &&
32             typeof _super[name] == "function" && fnTest.test(prop[name]) ?
33             (function(name, fn){
34               return function() {
35                 var tmp = this._super;
36                 
37                 // Add a new ._super() method that is the same method but on the
38                 // super-class
39                 this._super = _super[name];
40                 
41                 // The method only need to be bound temporarily, so we remove it
42                 // when we're done executing
43                 var ret = fn.apply(this, arguments); this._super = tmp;
44                 
45                 return ret;
46               };
47             })(name, prop[name]) : prop[name];
48         }
49         
50         // The dummy class constructor
51         function Class() {
52             // All construction is actually done in the init method
53             if (!initializing && this.init) {
54                 var ret = this.init.apply(this, arguments); if (ret) { return ret;}
55             } return this;
56         }
57         
58         Class.include = function (properties) {
59             for (var name in properties) {
60                 if (typeof properties[name] !== 'function'
61                         || !fnTest.test(properties[name])) {
62                     prototype[name] = properties[name];
63                 } else if (typeof prototype[name] === 'function'
64                            && prototype.hasOwnProperty(name)) {
65                     prototype[name] = (function (name, fn, previous) {
66                         return function () {
67                             var tmp = this._super; this._super = previous; var
68                             ret = fn.apply(this, arguments); this._super = tmp;
69                             return ret;
70                         }
71                     })(name, properties[name], prototype[name]);
72                 } else if (typeof _super[name] === 'function') {
73                     prototype[name] = (function (name, fn) {
74                         return function () {
75                             var tmp = this._super; this._super = _super[name];
76                             var ret = fn.apply(this, arguments); this._super =
77                             tmp; return ret;
78                         }
79                     })(name, properties[name]);
80                 }
81             }
82         };
83         
84         // Populate our constructed prototype object
85         Class.prototype = prototype;
86         
87         // Enforce the constructor to be what we expect
88         Class.prototype.constructor = Class;
89     
90         // And make this class extendable
91         Class.extend = arguments.callee;
92         
93         return Class;
94     };
95 };
96 oe_override_class(nova.Class);
97 oe_override_class(nova.Widget);
98
99 openerp.web.Class = nova.Class;
100
101 openerp.web.callback = function(obj, method) {
102     var callback = function() {
103         var args = Array.prototype.slice.call(arguments);
104         var r;
105         for(var i = 0; i < callback.callback_chain.length; i++)  {
106             var c = callback.callback_chain[i];
107             if(c.unique) {
108                 callback.callback_chain.splice(i, 1);
109                 i -= 1;
110             }
111             var result = c.callback.apply(c.self, c.args.concat(args));
112             if (c.callback === method) {
113                 // return the result of the original method
114                 r = result;
115             }
116             // TODO special value to stop the chain
117             // openerp.web.callback_stop
118         }
119         return r;
120     };
121     callback.callback_chain = [];
122     callback.add = function(f) {
123         if(typeof(f) == 'function') {
124             f = { callback: f, args: Array.prototype.slice.call(arguments, 1) };
125         }
126         f.self = f.self || null;
127         f.args = f.args || [];
128         f.unique = !!f.unique;
129         if(f.position == 'last') {
130             callback.callback_chain.push(f);
131         } else {
132             callback.callback_chain.unshift(f);
133         }
134         return callback;
135     };
136     callback.add_first = function(f) {
137         return callback.add.apply(null,arguments);
138     };
139     callback.add_last = function(f) {
140         return callback.add({
141             callback: f,
142             args: Array.prototype.slice.call(arguments, 1),
143             position: "last"
144         });
145     };
146     callback.remove = function(f) {
147         callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) {
148             return el.callback === f;
149         }));
150         return callback;
151     };
152
153     return callback.add({
154         callback: method,
155         self:obj,
156         args:Array.prototype.slice.call(arguments, 2)
157     });
158 };
159
160 /**
161  * web error for lookup failure
162  *
163  * @class
164  */
165 openerp.web.NotFound = openerp.web.Class.extend( /** @lends openerp.web.NotFound# */ {
166 });
167 openerp.web.KeyNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.KeyNotFound# */ {
168     /**
169      * Thrown when a key could not be found in a mapping
170      *
171      * @constructs openerp.web.KeyNotFound
172      * @extends openerp.web.NotFound
173      * @param {String} key the key which could not be found
174      */
175     init: function (key) {
176         this.key = key;
177     },
178     toString: function () {
179         return "The key " + this.key + " was not found";
180     }
181 });
182 openerp.web.ObjectNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.ObjectNotFound# */ {
183     /**
184      * Thrown when an object path does not designate a valid class or object
185      * in the openerp hierarchy.
186      *
187      * @constructs openerp.web.ObjectNotFound
188      * @extends openerp.web.NotFound
189      * @param {String} path the invalid object path
190      */
191     init: function (path) {
192         this.path = path;
193     },
194     toString: function () {
195         return "Could not find any object of path " + this.path;
196     }
197 });
198 openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry# */ {
199     /**
200      * Stores a mapping of arbitrary key (strings) to object paths (as strings
201      * as well).
202      *
203      * Resolves those paths at query time in order to always fetch the correct
204      * object, even if those objects have been overloaded/replaced after the
205      * registry was created.
206      *
207      * An object path is simply a dotted name from the openerp root to the
208      * object pointed to (e.g. ``"openerp.web.Connection"`` for an OpenERP
209      * connection object).
210      *
211      * @constructs openerp.web.Registry
212      * @param {Object} mapping a mapping of keys to object-paths
213      */
214     init: function (mapping) {
215         this.parent = null;
216         this.map = mapping || {};
217     },
218     /**
219      * Retrieves the object matching the provided key string.
220      *
221      * @param {String} key the key to fetch the object for
222      * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception
223      * @returns {Class} the stored class, to initialize
224      *
225      * @throws {openerp.web.KeyNotFound} if the object was not in the mapping
226      * @throws {openerp.web.ObjectNotFound} if the object path was invalid
227      */
228     get_object: function (key, silent_error) {
229         var path_string = this.map[key];
230         if (path_string === undefined) {
231             if (this.parent) {
232                 return this.parent.get_object(key, silent_error);
233             }
234             if (silent_error) { return void 'nooo'; }
235             throw new openerp.web.KeyNotFound(key);
236         }
237
238         var object_match = openerp;
239         var path = path_string.split('.');
240         // ignore first section
241         for(var i=1; i<path.length; ++i) {
242             object_match = object_match[path[i]];
243
244             if (object_match === undefined) {
245                 if (silent_error) { return void 'noooooo'; }
246                 throw new openerp.web.ObjectNotFound(path_string);
247             }
248         }
249         return object_match;
250     },
251     /**
252      * Checks if the registry contains an object mapping for this key.
253      *
254      * @param {String} key key to look for
255      */
256     contains: function (key) {
257         if (key === undefined) { return false; }
258         if (key in this.map) {
259             return true
260         }
261         if (this.parent) {
262             return this.parent.contains(key);
263         }
264         return false;
265     },
266     /**
267      * Tries a number of keys, and returns the first object matching one of
268      * the keys.
269      *
270      * @param {Array} keys a sequence of keys to fetch the object for
271      * @returns {Class} the first class found matching an object
272      *
273      * @throws {openerp.web.KeyNotFound} if none of the keys was in the mapping
274      * @trows {openerp.web.ObjectNotFound} if a found object path was invalid
275      */
276     get_any: function (keys) {
277         for (var i=0; i<keys.length; ++i) {
278             var key = keys[i];
279             if (!this.contains(key)) {
280                 continue;
281             }
282
283             return this.get_object(key);
284         }
285         throw new openerp.web.KeyNotFound(keys.join(','));
286     },
287     /**
288      * Adds a new key and value to the registry.
289      *
290      * This method can be chained.
291      *
292      * @param {String} key
293      * @param {String} object_path fully qualified dotted object path
294      * @returns {openerp.web.Registry} itself
295      */
296     add: function (key, object_path) {
297         this.map[key] = object_path;
298         return this;
299     },
300     /**
301      * Creates and returns a copy of the current mapping, with the provided
302      * mapping argument added in (replacing existing keys if needed)
303      *
304      * Parent and child remain linked, a new key in the parent (which is not
305      * overwritten by the child) will appear in the child.
306      *
307      * @param {Object} [mapping={}] a mapping of keys to object-paths
308      */
309     extend: function (mapping) {
310         var child = new openerp.web.Registry(mapping);
311         child.parent = this;
312         return child;
313     },
314     /**
315      * @deprecated use Registry#extend
316      */
317     clone: function (mapping) {
318         console.warn('Registry#clone is deprecated, use Registry#extend');
319         return this.extend(mapping);
320     }
321 });
322
323 openerp.web.CallbackEnabledMixin = {
324     init: function() {
325         // Transform on_* method into openerp.web.callbacks
326         for (var name in this) {
327             if(typeof(this[name]) == "function") {
328                 this[name].debug_name = name;
329                 // bind ALL function to this not only on_and _do ?
330                 if((/^on_|^do_/).test(name)) {
331                     this[name] = openerp.web.callback(this, this[name]);
332                 }
333             }
334         }
335     },
336     /**
337      * Proxies a method of the object, in order to keep the right ``this`` on
338      * method invocations.
339      *
340      * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
341      * even more so to ``jQuery.proxy`` with a fundamental difference: its
342      * resolution of the method being called is lazy, meaning it will use the
343      * method as it is when the proxy is called, not when the proxy is created.
344      *
345      * Other methods will fix the bound method to what it is when creating the
346      * binding/proxy, which is fine in most javascript code but problematic in
347      * OpenERP Web where developers may want to replace existing callbacks with
348      * theirs.
349      *
350      * The semantics of this precisely replace closing over the method call.
351      *
352      * @param {String} method_name name of the method to invoke
353      * @returns {Function} proxied method
354      */
355     proxy: function (method_name) {
356         var self = this;
357         return function () {
358             return self[method_name].apply(self, arguments);
359         }
360     }
361 };
362
363 openerp.web.CallbackEnabled = openerp.web.Class.extend(_.extend({}, nova.GetterSetterMixin, openerp.web.CallbackEnabledMixin, {
364     init: function() {
365         nova.GetterSetterMixin.init.call(this);
366         openerp.web.CallbackEnabledMixin.init.call(this);
367     }
368 }));
369
370 openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp.web.Connection# */{
371     /**
372      * @constructs openerp.web.Connection
373      * @extends openerp.web.CallbackEnabled
374      *
375      * @param {String} [server] JSON-RPC endpoint hostname
376      * @param {String} [port] JSON-RPC endpoint port
377      */
378     init: function() {
379         this._super();
380         this.server = null;
381         this.debug = ($.deparam($.param.querystring()).debug != undefined);
382         // TODO: session store in cookie should be optional
383         this.name = openerp._session_id;
384         this.qweb_mutex = new $.Mutex();
385     },
386     bind_session: function(origin) {
387         var window_origin = location.protocol+"//"+location.host, self=this;
388         this.origin = origin ? _.str.rtrim(origin,'/') : window_origin;
389         this.prefix = this.origin;
390         this.server = this.origin; // keep chs happy
391         openerp.web.qweb.default_dict['_s'] = this.origin;
392         this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp;
393         this.session_id = false;
394         this.uid = false;
395         this.username = false;
396         this.user_context= {};
397         this.db = false;
398         this.openerp_entreprise = false;
399         this.module_list = openerp._modules.slice();
400         this.module_loaded = {};
401         _(this.module_list).each(function (mod) {
402             self.module_loaded[mod] = true;
403         });
404         this.context = {};
405         this.shortcuts = [];
406         this.active_id = null;
407         return this.session_init();
408     },
409     test_eval_get_context: function () {
410         var asJS = function (arg) {
411             if (arg instanceof py.object) {
412                 return arg.toJSON();
413             }
414             return arg;
415         };
416
417         var datetime = new py.object();
418         datetime.datetime = new py.type(function datetime() {
419             throw new Error('datetime.datetime not implemented');
420         });
421         var date = datetime.date = new py.type(function date(y, m, d) {
422             if (y instanceof Array) {
423                 d = y[2];
424                 m = y[1];
425                 y = y[0];
426             }
427             this.year = asJS(y);
428             this.month = asJS(m);
429             this.day = asJS(d);
430         }, py.object, {
431             strftime: function (args) {
432                 var f = asJS(args[0]), self = this;
433                 return new py.str(f.replace(/%([A-Za-z])/g, function (m, c) {
434                     switch (c) {
435                     case 'Y': return self.year;
436                     case 'm': return _.str.sprintf('%02d', self.month);
437                     case 'd': return _.str.sprintf('%02d', self.day);
438                     }
439                     throw new Error('ValueError: No known conversion for ' + m);
440                 }));
441             }
442         });
443         date.__getattribute__ = function (name) {
444             if (name === 'today') {
445                 return date.today;
446             }
447             throw new Error("AttributeError: object 'date' has no attribute '" + name +"'");
448         };
449         date.today = new py.def(function () {
450             var d = new Date();
451             return new date(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
452         });
453         datetime.time = new py.type(function time() {
454             throw new Error('datetime.time not implemented');
455         });
456
457         var time = new py.object();
458         time.strftime = new py.def(function (args) {
459             return date.today.__call__().strftime(args);
460         });
461
462         var relativedelta = new py.type(function relativedelta(args, kwargs) {
463             if (!_.isEmpty(args)) {
464                 throw new Error('Extraction of relative deltas from existing datetimes not supported');
465             }
466             this.ops = kwargs;
467         }, py.object, {
468             __add__: function (other) {
469                 if (!(other instanceof datetime.date)) {
470                     return py.NotImplemented;
471                 }
472                 // TODO: test this whole mess
473                 var year = asJS(this.ops.year) || asJS(other.year);
474                 if (asJS(this.ops.years)) {
475                     year += asJS(this.ops.years);
476                 }
477
478                 var month = asJS(this.ops.month) || asJS(other.month);
479                 if (asJS(this.ops.months)) {
480                     month += asJS(this.ops.months);
481                     // FIXME: no divmod in JS?
482                     while (month < 1) {
483                         year -= 1;
484                         month += 12;
485                     }
486                     while (month > 12) {
487                         year += 1;
488                         month -= 12;
489                     }
490                 }
491
492                 var lastMonthDay = new Date(year, month, 0).getDate();
493                 var day = asJS(this.ops.day) || asJS(other.day);
494                 if (day > lastMonthDay) { day = lastMonthDay; }
495                 var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
496                 if (days_offset) {
497                     day = new Date(year, month-1, day + days_offset).getDate();
498                 }
499                 // TODO: leapdays?
500                 // TODO: hours, minutes, seconds? Not used in XML domains
501                 // TODO: weekday?
502                 return new datetime.date(year, month, day);
503             },
504             __radd__: function (other) {
505                 return this.__add__(other);
506             },
507
508             __sub__: function (other) {
509                 if (!(other instanceof datetime.date)) {
510                     return py.NotImplemented;
511                 }
512                 // TODO: test this whole mess
513                 var year = asJS(this.ops.year) || asJS(other.year);
514                 if (asJS(this.ops.years)) {
515                     year -= asJS(this.ops.years);
516                 }
517
518                 var month = asJS(this.ops.month) || asJS(other.month);
519                 if (asJS(this.ops.months)) {
520                     month -= asJS(this.ops.months);
521                     // FIXME: no divmod in JS?
522                     while (month < 1) {
523                         year -= 1;
524                         month += 12;
525                     }
526                     while (month > 12) {
527                         year += 1;
528                         month -= 12;
529                     }
530                 }
531
532                 var lastMonthDay = new Date(year, month, 0).getDate();
533                 var day = asJS(this.ops.day) || asJS(other.day);
534                 if (day > lastMonthDay) { day = lastMonthDay; }
535                 var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
536                 if (days_offset) {
537                     day = new Date(year, month-1, day - days_offset).getDate();
538                 }
539                 // TODO: leapdays?
540                 // TODO: hours, minutes, seconds? Not used in XML domains
541                 // TODO: weekday?
542                 return new datetime.date(year, month, day);
543             },
544             __rsub__: function (other) {
545                 return this.__sub__(other);
546             }
547         });
548
549         return {
550             uid: new py.float(this.uid),
551             datetime: datetime,
552             time: time,
553             relativedelta: relativedelta
554         };
555     },
556     /**
557      * FIXME: Huge testing hack, especially the evaluation context, rewrite + test for real before switching
558      */
559     test_eval: function (source, expected) {
560         try {
561             var ctx = this.test_eval_contexts(source.contexts);
562             if (!_.isEqual(ctx, expected.context)) {
563                 console.group('Local context does not match remote, nothing is broken but please report to R&D (xmo)');
564                 console.warn('source', source.contexts);
565                 console.warn('local', ctx);
566                 console.warn('remote', expected.context);
567                 console.groupEnd();
568             }
569         } catch (e) {
570             console.group('Failed to evaluate contexts, nothing is broken but please report to R&D (xmo)');
571             console.error(e);
572             console.log('source', source.contexts);
573             console.groupEnd();
574         }
575
576         try {
577             var dom = this.test_eval_domains(source.domains, this.test_eval_get_context());
578             if (!_.isEqual(dom, expected.domain)) {
579                 console.group('Local domain does not match remote, nothing is broken but please report to R&D (xmo)');
580                 console.warn('source', source.domains);
581                 console.warn('local', dom);
582                 console.warn('remote', expected.domain);
583                 console.groupEnd();
584             }
585         } catch (e) {
586             console.group('Failed to evaluate domains, nothing is broken but please report to R&D (xmo)');
587             console.error(e);
588             console.log('source', source.domains);
589             console.groupEnd();
590         }
591
592         try {
593             var groups = this.test_eval_groupby(source.group_by_seq);
594             if (!_.isEqual(groups, expected.group_by)) {
595                 console.group('Local groupby does not match remote, nothing is broken but please report to R&D (xmo)');
596                 console.warn('source', source.group_by_seq);
597                 console.warn('local', groups);
598                 console.warn('remote', expected.group_by);
599                 console.groupEnd();
600             }
601         } catch (e) {
602             console.group('Failed to evaluate groupby, nothing is broken but please report to R&D (xmo)');
603             console.error(e);
604             console.log('source', source.group_by_seq);
605             console.groupEnd();
606         }
607     },
608     test_eval_contexts: function (contexts) {
609         var result_context = _.extend({}, this.user_context),
610             self = this;
611         _(contexts).each(function (ctx) {
612             switch(ctx.__ref) {
613             case 'context':
614                 _.extend(result_context, py.eval(ctx.__debug));
615                 break;
616             case 'compound_context':
617                 _.extend(
618                     result_context, self.test_eval_contexts(ctx.__contexts));
619                 break;
620             default:
621                 _.extend(result_context, ctx);
622             }
623         });
624         return result_context;
625     },
626     test_eval_domains: function (domains, eval_context) {
627         var result_domain = [], self = this;
628         _(domains).each(function (dom) {
629             switch(dom.__ref) {
630             case 'domain':
631                 result_domain.push.apply(
632                     result_domain, py.eval(dom.__debug, eval_context));
633                 break;
634             case 'compound_domain':
635                 result_domain.push.apply(
636                     result_domain, self.test_eval_domains(
637                             dom.__domains, eval_context));
638                 break;
639             default:
640                 result_domain.push.apply(
641                     result_domain, dom);
642             }
643         });
644         return result_domain;
645     },
646     test_eval_groupby: function (contexts) {
647         var result_group = [], self = this;
648         _(contexts).each(function (ctx) {
649             var group;
650             switch(ctx.__ref) {
651             case 'context':
652                 group = py.eval(ctx.__debug).group_by;
653                 break;
654             case 'compound_context':
655                 group = self.test_eval_contexts(ctx.__contexts).group_by;
656                 break;
657             default:
658                 group = ctx.group_by
659             }
660             if (!group) { return; }
661             if (typeof group === 'string') {
662                 result_group.push(group);
663             } else if (group instanceof Array) {
664                 result_group.push.apply(result_group, group);
665             } else {
666                 throw new Error('Got invalid groupby {{'
667                         + JSON.stringify(group) + '}}');
668             }
669         });
670         return result_group;
671     },
672     /**
673      * Executes an RPC call, registering the provided callbacks.
674      *
675      * Registers a default error callback if none is provided, and handles
676      * setting the correct session id and session context in the parameter
677      * objects
678      *
679      * @param {String} url RPC endpoint
680      * @param {Object} params call parameters
681      * @param {Function} success_callback function to execute on RPC call success
682      * @param {Function} error_callback function to execute on RPC call failure
683      * @returns {jQuery.Deferred} jquery-provided ajax deferred
684      */
685     rpc: function(url, params, success_callback, error_callback) {
686         var self = this;
687         // url can be an $.ajax option object
688         if (_.isString(url)) {
689             url = { url: url };
690         }
691         // Construct a JSON-RPC2 request, method is currently unused
692         params.session_id = this.session_id;
693         if (this.debug)
694             params.debug = 1;
695         var payload = {
696             jsonrpc: '2.0',
697             method: 'call',
698             params: params,
699             id: _.uniqueId('r')
700         };
701         var deferred = $.Deferred();
702         this.on_rpc_request();
703         var aborter = params.aborter;
704         delete params.aborter;
705         var request = this.rpc_function(url, payload).then(
706             function (response, textStatus, jqXHR) {
707                 self.on_rpc_response();
708                 if (!response.error) {
709                     if (url.url === '/web/session/eval_domain_and_context') {
710                         self.test_eval(params, response.result);
711                     }
712                     deferred.resolve(response["result"], textStatus, jqXHR);
713                 } else if (response.error.data.type === "session_invalid") {
714                     self.uid = false;
715                     // TODO deprecate or use a deferred on login.do_ask_login()
716                     self.on_session_invalid(function() {
717                         self.rpc(url, payload.params,
718                             function() { deferred.resolve.apply(deferred, arguments); },
719                             function() { deferred.reject.apply(deferred, arguments); });
720                     });
721                 } else {
722                     deferred.reject(response.error, $.Event());
723                 }
724             },
725             function(jqXHR, textStatus, errorThrown) {
726                 self.on_rpc_response();
727                 var error = {
728                     code: -32098,
729                     message: "XmlHttpRequestError " + errorThrown,
730                     data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] }
731                 };
732                 deferred.reject(error, $.Event());
733             });
734         if (aborter) {
735             aborter.abort_last = function () {
736                 if (!(request.isResolved() || request.isRejected())) {
737                     deferred.fail(function (error, event) {
738                         event.preventDefault();
739                     });
740                     request.abort();
741                 }
742             };
743         }
744         // Allow deferred user to disable on_rpc_error in fail
745         deferred.fail(function() {
746             deferred.fail(function(error, event) {
747                 if (!event.isDefaultPrevented()) {
748                     self.on_rpc_error(error, event);
749                 }
750             });
751         }).then(success_callback, error_callback).promise();
752         return deferred;
753     },
754     /**
755      * Raw JSON-RPC call
756      *
757      * @returns {jQuery.Deferred} ajax-webd deferred object
758      */
759     rpc_json: function(url, payload) {
760         var self = this;
761         var ajax = _.extend({
762             type: "POST",
763             dataType: 'json',
764             contentType: 'application/json',
765             data: JSON.stringify(payload),
766             processData: false
767         }, url);
768         if (this.synch)
769                 ajax.async = false;
770         return $.ajax(ajax);
771     },
772     rpc_jsonp: function(url, payload) {
773         var self = this;
774         // extracted from payload to set on the url
775         var data = {
776             session_id: this.session_id,
777             id: payload.id
778         };
779         url.url = this.get_url(url.url);
780         var ajax = _.extend({
781             type: "GET",
782             dataType: 'jsonp', 
783             jsonp: 'jsonp',
784             cache: false,
785             data: data
786         }, url);
787         if (this.synch)
788                 ajax.async = false;
789         var payload_str = JSON.stringify(payload);
790         var payload_url = $.param({r:payload_str});
791         if(payload_url.length < 2000) {
792             // Direct jsonp request
793             ajax.data.r = payload_str;
794             return $.ajax(ajax);
795         } else {
796             // Indirect jsonp request
797             var ifid = _.uniqueId('oe_rpc_iframe');
798             var display = options.openerp.debug ? 'block' : 'none';
799             var $iframe = $(_.str.sprintf("<iframe src='javascript:false;' name='%s' id='%s' style='display:%s'></iframe>", ifid, ifid, display));
800             var $form = $('<form>')
801                         .attr('method', 'POST')
802                         .attr('target', ifid)
803                         .attr('enctype', "multipart/form-data")
804                         .attr('action', ajax.url + '?' + $.param(data))
805                         .append($('<input type="hidden" name="r" />').attr('value', payload_str))
806                         .hide()
807                         .appendTo($('body'));
808             var cleanUp = function() {
809                 if ($iframe) {
810                     $iframe.unbind("load").attr("src", "javascript:false;").remove();
811                 }
812                 $form.remove();
813             };
814             var deferred = $.Deferred();
815             // the first bind is fired up when the iframe is added to the DOM
816             $iframe.bind('load', function() {
817                 // the second bind is fired up when the result of the form submission is received
818                 $iframe.unbind('load').bind('load', function() {
819                     $.ajax(ajax).always(function() {
820                         cleanUp();
821                     }).then(
822                         function() { deferred.resolve.apply(deferred, arguments); },
823                         function() { deferred.reject.apply(deferred, arguments); }
824                     );
825                 });
826                 // now that the iframe can receive data, we fill and submit the form
827                 $form.submit();
828             });
829             // append the iframe to the DOM (will trigger the first load)
830             $form.after($iframe);
831             return deferred;
832         }
833     },
834     on_rpc_request: function() {
835     },
836     on_rpc_response: function() {
837     },
838     on_rpc_error: function(error) {
839     },
840     /**
841      * Init a session, reloads from cookie, if it exists
842      */
843     session_init: function () {
844         var self = this;
845         // TODO: session store in cookie should be optional
846         this.session_id = this.get_cookie('session_id');
847         return this.session_reload().pipe(function(result) {
848             var modules = openerp._modules.join(',');
849             var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).pipe(self.do_load_qweb);
850             if(self.session_is_valid()) {
851                 return deferred.pipe(function() { return self.load_modules(); });
852             }
853             return deferred;
854         });
855     },
856     /**
857      * (re)loads the content of a session: db name, username, user id, session
858      * context and status of the support contract
859      *
860      * @returns {$.Deferred} deferred indicating the session is done reloading
861      */
862     session_reload: function () {
863         var self = this;
864         return this.rpc("/web/session/get_session_info", {}).then(function(result) {
865             // If immediately follows a login (triggered by trying to restore
866             // an invalid session or no session at all), refresh session data
867             // (should not change, but just in case...)
868             _.extend(self, {
869                 db: result.db,
870                 username: result.login,
871                 uid: result.uid,
872                 user_context: result.context,
873                 openerp_entreprise: result.openerp_entreprise
874             });
875         });
876     },
877     session_is_valid: function() {
878         return !!this.uid;
879     },
880     /**
881      * The session is validated either by login or by restoration of a previous session
882      */
883     session_authenticate: function(db, login, password, _volatile) {
884         var self = this;
885         var base_location = document.location.protocol + '//' + document.location.host;
886         var params = { db: db, login: login, password: password, base_location: base_location };
887         return this.rpc("/web/session/authenticate", params).pipe(function(result) {
888             _.extend(self, {
889                 session_id: result.session_id,
890                 db: result.db,
891                 username: result.login,
892                 uid: result.uid,
893                 user_context: result.context,
894                 openerp_entreprise: result.openerp_entreprise
895             });
896             if (!_volatile) {
897                 self.set_cookie('session_id', self.session_id);
898             }
899             return self.load_modules();
900         });
901     },
902     session_logout: function() {
903         this.set_cookie('session_id', '');
904         return this.rpc("/web/session/destroy", {});
905     },
906     on_session_valid: function() {
907     },
908     /**
909      * Called when a rpc call fail due to an invalid session.
910      * By default, it's a noop
911      */
912     on_session_invalid: function(retry_callback) {
913     },
914     /**
915      * Fetches a cookie stored by an openerp session
916      *
917      * @private
918      * @param name the cookie's name
919      */
920     get_cookie: function (name) {
921         if (!this.name) { return null; }
922         var nameEQ = this.name + '|' + name + '=';
923         var cookies = document.cookie.split(';');
924         for(var i=0; i<cookies.length; ++i) {
925             var cookie = cookies[i].replace(/^\s*/, '');
926             if(cookie.indexOf(nameEQ) === 0) {
927                 return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
928             }
929         }
930         return null;
931     },
932     /**
933      * Create a new cookie with the provided name and value
934      *
935      * @private
936      * @param name the cookie's name
937      * @param value the cookie's value
938      * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete
939      */
940     set_cookie: function (name, value, ttl) {
941         if (!this.name) { return; }
942         ttl = ttl || 24*60*60*365;
943         document.cookie = [
944             this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
945             'path=/',
946             'max-age=' + ttl,
947             'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
948         ].join(';');
949     },
950     /**
951      * Load additional web addons of that instance and init them
952      *
953      * @param {Boolean} [no_session_valid_signal=false] prevents load_module from triggering ``on_session_valid``.
954      */
955     load_modules: function(no_session_valid_signal) {
956         var self = this;
957         return this.rpc('/web/session/modules', {}).pipe(function(result) {
958             var lang = self.user_context.lang,
959                 all_modules = _.uniq(self.module_list.concat(result));
960             var params = { mods: all_modules, lang: lang};
961             var to_load = _.difference(result, self.module_list).join(',');
962             self.module_list = all_modules;
963
964             var loaded = $.Deferred().resolve().promise();
965             if (to_load.length) {
966                 loaded = $.when(
967                     self.rpc('/web/webclient/csslist', {mods: to_load}, self.do_load_css),
968                     self.rpc('/web/webclient/qweblist', {mods: to_load}).pipe(self.do_load_qweb),
969                     self.rpc('/web/webclient/translations', params).pipe(function(trans) {
970                         openerp.web._t.database.set_bundle(trans);
971                         var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"];
972                         return self.rpc('/web/webclient/jslist', {mods: to_load}).pipe(function(files) {
973                             return self.do_load_js(file_list.concat(files));
974                         }).then(function () {
975                             if (!Date.CultureInfo.pmDesignator) {
976                                 // If no am/pm designator is specified but the openerp
977                                 // datetime format uses %i, date.js won't be able to
978                                 // correctly format a date. See bug#938497.
979                                 Date.CultureInfo.amDesignator = 'AM';
980                                 Date.CultureInfo.pmDesignator = 'PM';
981                             }
982                         });
983                     }))
984             }
985             return loaded.then(function() {
986                 self.on_modules_loaded();
987                 if (!no_session_valid_signal) {
988                     self.on_session_valid();
989                 }
990             });
991         });
992     },
993     do_load_css: function (files) {
994         var self = this;
995         _.each(files, function (file) {
996             $('head').append($('<link>', {
997                 'href': self.get_url(file),
998                 'rel': 'stylesheet',
999                 'type': 'text/css'
1000             }));
1001         });
1002     },
1003     do_load_js: function(files) {
1004         var self = this;
1005         var d = $.Deferred();
1006         if(files.length != 0) {
1007             var file = files.shift();
1008             var tag = document.createElement('script');
1009             tag.type = 'text/javascript';
1010             tag.src = self.get_url(file);
1011             tag.onload = tag.onreadystatechange = function() {
1012                 if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done )
1013                     return;
1014                 tag.onload_done = true;
1015                 self.do_load_js(files).then(function () {
1016                     d.resolve();
1017                 });
1018             };
1019             var head = document.head || document.getElementsByTagName('head')[0];
1020             head.appendChild(tag);
1021         } else {
1022             d.resolve();
1023         }
1024         return d;
1025     },
1026     do_load_qweb: function(files) {
1027         var self = this;
1028         _.each(files, function(file) {
1029             self.qweb_mutex.exec(function() {
1030                 return self.rpc('/web/proxy/load', {path: file}).pipe(function(xml) {
1031                     if (!xml) { return; }
1032                     openerp.web.qweb.add_template(_.str.trim(xml));
1033                 });
1034             });
1035         });
1036         return self.qweb_mutex.def;
1037     },
1038     on_modules_loaded: function() {
1039         for(var j=0; j<this.module_list.length; j++) {
1040             var mod = this.module_list[j];
1041             if(this.module_loaded[mod])
1042                 continue;
1043             openerp[mod] = {};
1044             // init module mod
1045             if(openerp._openerp[mod] != undefined) {
1046                 openerp._openerp[mod](openerp);
1047                 this.module_loaded[mod] = true;
1048             }
1049         }
1050     },
1051     get_url: function (file) {
1052         return this.prefix + file;
1053     },
1054     /**
1055      * Cooperative file download implementation, for ajaxy APIs.
1056      *
1057      * Requires that the server side implements an httprequest correctly
1058      * setting the `fileToken` cookie to the value provided as the `token`
1059      * parameter. The cookie *must* be set on the `/` path and *must not* be
1060      * `httpOnly`.
1061      *
1062      * It would probably also be a good idea for the response to use a
1063      * `Content-Disposition: attachment` header, especially if the MIME is a
1064      * "known" type (e.g. text/plain, or for some browsers application/json
1065      *
1066      * @param {Object} options
1067      * @param {String} [options.url] used to dynamically create a form
1068      * @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
1069      * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file
1070      * @param {Function} [options.success] callback in case of download success
1071      * @param {Function} [options.error] callback in case of request error, provided with the error body
1072      * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed
1073      */
1074     get_file: function (options) {
1075         // need to detect when the file is done downloading (not used
1076         // yet, but we'll need it to fix the UI e.g. with a throbber
1077         // while dump is being generated), iframe load event only fires
1078         // when the iframe content loads, so we need to go smarter:
1079         // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx
1080         var timer, token = new Date().getTime(),
1081             cookie_name = 'fileToken', cookie_length = cookie_name.length,
1082             CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'),
1083             remove_form = false;
1084
1085         var $form, $form_data = $('<div>');
1086
1087         var complete = function () {
1088             if (options.complete) { options.complete(); }
1089             clearTimeout(timer);
1090             $form_data.remove();
1091             $target.remove();
1092             if (remove_form && $form) { $form.remove(); }
1093         };
1094         var $target = $('<iframe style="display: none;">')
1095             .attr({id: id, name: id})
1096             .appendTo(document.body)
1097             .load(function () {
1098                 try {
1099                     if (options.error) {
1100                         options.error(JSON.parse(
1101                             this.contentDocument.body.childNodes[1].textContent
1102                         ));
1103                     }
1104                 } finally {
1105                     complete();
1106                 }
1107             });
1108
1109         if (options.form) {
1110             $form = $(options.form);
1111         } else {
1112             remove_form = true;
1113             $form = $('<form>', {
1114                 action: options.url,
1115                 method: 'POST'
1116             }).appendTo(document.body);
1117         }
1118
1119         _(_.extend({}, options.data || {},
1120                    {session_id: this.session_id, token: token}))
1121             .each(function (value, key) {
1122                 var $input = $form.find('[name=' + key +']');
1123                 if (!$input.length) {
1124                     $input = $('<input type="hidden" name="' + key + '">')
1125                         .appendTo($form_data);
1126                 }
1127                 $input.val(value)
1128             });
1129
1130         $form
1131             .append($form_data)
1132             .attr('target', id)
1133             .get(0).submit();
1134
1135         var waitLoop = function () {
1136             var cookies = document.cookie.split(';');
1137             // setup next check
1138             timer = setTimeout(waitLoop, CHECK_INTERVAL);
1139             for (var i=0; i<cookies.length; ++i) {
1140                 var cookie = cookies[i].replace(/^\s*/, '');
1141                 if (!cookie.indexOf(cookie_name === 0)) { continue; }
1142                 var cookie_val = cookie.substring(cookie_length + 1);
1143                 if (parseInt(cookie_val, 10) !== token) { continue; }
1144
1145                 // clear cookie
1146                 document.cookie = _.str.sprintf("%s=;expires=%s;path=/",
1147                     cookie_name, new Date().toGMTString());
1148                 if (options.success) { options.success(); }
1149                 complete();
1150                 return;
1151             }
1152         };
1153         timer = setTimeout(waitLoop, CHECK_INTERVAL);
1154     },
1155     synchronized_mode: function(to_execute) {
1156         var synch = this.synch;
1157         this.synch = true;
1158         try {
1159                 return to_execute();
1160         } finally {
1161                 this.synch = synch;
1162         }
1163     }
1164 });
1165
1166 /**
1167  * Base class for all visual components. Provides a lot of functionalities helpful
1168  * for the management of a part of the DOM.
1169  *
1170  * Widget handles:
1171  * - Rendering with QWeb.
1172  * - Life-cycle management and parenting (when a parent is destroyed, all its children are
1173  *     destroyed too).
1174  * - Insertion in DOM.
1175  *
1176  * Guide to create implementations of the Widget class:
1177  * ==============================================
1178  *
1179  * Here is a sample child class:
1180  *
1181  * MyWidget = openerp.base.Widget.extend({
1182  *     // the name of the QWeb template to use for rendering
1183  *     template: "MyQWebTemplate",
1184  *
1185  *     init: function(parent) {
1186  *         this._super(parent);
1187  *         // stuff that you want to init before the rendering
1188  *     },
1189  *     start: function() {
1190  *         // stuff you want to make after the rendering, `this.$element` holds a correct value
1191  *         this.$element.find(".my_button").click(/* an example of event binding * /);
1192  *
1193  *         // if you have some asynchronous operations, it's a good idea to return
1194  *         // a promise in start()
1195  *         var promise = this.rpc(...);
1196  *         return promise;
1197  *     }
1198  * });
1199  *
1200  * Now this class can simply be used with the following syntax:
1201  *
1202  * var my_widget = new MyWidget(this);
1203  * my_widget.appendTo($(".some-div"));
1204  *
1205  * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
1206  * DOM inside the ".some-div" div and its events were binded.
1207  *
1208  * And of course, when you don't need that widget anymore, just do:
1209  *
1210  * my_widget.destroy();
1211  *
1212  * That will kill the widget in a clean way and erase its content from the dom.
1213  */
1214 openerp.web.Widget = nova.Widget.extend(_.extend({}, openerp.web.CallbackEnabledMixin, {
1215     /**
1216      * The name of the QWeb template that will be used for rendering. Must be
1217      * redefined in subclasses or the default render() method can not be used.
1218      *
1219      * @type string
1220      */
1221     template: null,
1222     /**
1223      * Constructs the widget and sets its parent if a parent is given.
1224      *
1225      * @constructs openerp.web.Widget
1226      * @extends openerp.web.CallbackEnabled
1227      *
1228      * @param {openerp.web.Widget} parent Binds the current instance to the given Widget instance.
1229      * When that widget is destroyed by calling destroy(), the current instance will be
1230      * destroyed too. Can be null.
1231      * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want
1232      * to bind the current Widget to an already existing part of the DOM, which is not compatible
1233      * with the DOM insertion methods provided by the current implementation of Widget. So
1234      * for new components this argument should not be provided any more.
1235      */
1236     init: function(parent) {
1237         this._super(parent);
1238         openerp.web.CallbackEnabledMixin.init.call(this);
1239         this.session = openerp.connection;
1240     },
1241     /**
1242      * Renders the element. The default implementation renders the widget using QWeb,
1243      * `this.template` must be defined. The context given to QWeb contains the "widget"
1244      * key that references `this`.
1245      */
1246     renderElement: function() {
1247         var rendered = null;
1248         if (this.template)
1249             rendered = openerp.web.qweb.render(this.template, {widget: this});
1250         if (_.str.trim(rendered)) {
1251             var elem = $(rendered);
1252             this.$element.replaceWith(elem);
1253             this.$element = elem;
1254         }
1255     },
1256     /**
1257      * Informs the action manager to do an action. This supposes that
1258      * the action manager can be found amongst the ancestors of the current widget.
1259      * If that's not the case this method will simply return `false`.
1260      */
1261     do_action: function(action, on_finished) {
1262         if (this.getParent()) {
1263             return this.getParent().do_action(action, on_finished);
1264         }
1265         return false;
1266     },
1267     do_notify: function() {
1268         if (this.getParent()) {
1269             return this.getParent().do_notify.apply(this,arguments);
1270         }
1271         return false;
1272     },
1273     do_warn: function() {
1274         if (this.getParent()) {
1275             return this.getParent().do_warn.apply(this,arguments);
1276         }
1277         return false;
1278     },
1279     rpc: function(url, data, success, error) {
1280         var def = $.Deferred().then(success, error);
1281         var self = this;
1282         openerp.connection.rpc(url, data). then(function() {
1283             if (!self.isDestroyed())
1284                 def.resolve.apply(def, arguments);
1285         }, function() {
1286             if (!self.isDestroyed())
1287                 def.reject.apply(def, arguments);
1288         });
1289         return def.promise();
1290     }
1291 }));
1292
1293 /**
1294  * @deprecated use :class:`openerp.web.Widget`
1295  */
1296 openerp.web.OldWidget = openerp.web.Widget.extend({
1297     init: function(parent, element_id) {
1298         this._super(parent);
1299         this.element_id = element_id;
1300         this.element_id = this.element_id || _.uniqueId('widget-');
1301         var tmp = document.getElementById(this.element_id);
1302         this.$element = tmp ? $(tmp) : $(document.createElement(this.tagName));
1303     },
1304     renderElement: function() {
1305         var rendered = this.render();
1306         if (rendered) {
1307             var elem = $(rendered);
1308             this.$element.replaceWith(elem);
1309             this.$element = elem;
1310         }
1311         return this;
1312     },
1313     render: function (additional) {
1314         if (this.template)
1315             return openerp.web.qweb.render(this.template, _.extend({widget: this}, additional || {}));
1316         return null;
1317     }
1318 });
1319
1320 openerp.web.TranslationDataBase = openerp.web.Class.extend(/** @lends openerp.web.TranslationDataBase# */{
1321     /**
1322      * @constructs openerp.web.TranslationDataBase
1323      * @extends openerp.web.Class
1324      */
1325     init: function() {
1326         this.db = {};
1327         this.parameters = {"direction": 'ltr',
1328                         "date_format": '%m/%d/%Y',
1329                         "time_format": '%H:%M:%S',
1330                         "grouping": [],
1331                         "decimal_point": ".",
1332                         "thousands_sep": ","};
1333     },
1334     set_bundle: function(translation_bundle) {
1335         var self = this;
1336         this.db = {};
1337         var modules = _.keys(translation_bundle.modules);
1338         modules.sort();
1339         if (_.include(modules, "web")) {
1340             modules = ["web"].concat(_.without(modules, "web"));
1341         }
1342         _.each(modules, function(name) {
1343             self.add_module_translation(translation_bundle.modules[name]);
1344         });
1345         if (translation_bundle.lang_parameters) {
1346             this.parameters = translation_bundle.lang_parameters;
1347             this.parameters.grouping = py.eval(
1348                     this.parameters.grouping);
1349         }
1350     },
1351     add_module_translation: function(mod) {
1352         var self = this;
1353         _.each(mod.messages, function(message) {
1354             self.db[message.id] = message.string;
1355         });
1356     },
1357     build_translation_function: function() {
1358         var self = this;
1359         var fcnt = function(str) {
1360             var tmp = self.get(str);
1361             return tmp === undefined ? str : tmp;
1362         };
1363         fcnt.database = this;
1364         return fcnt;
1365     },
1366     get: function(key) {
1367         if (this.db[key])
1368             return this.db[key];
1369         return undefined;
1370     }
1371 });
1372
1373 /** Configure blockui */
1374 if ($.blockUI) {
1375     $.blockUI.defaults.baseZ = 1100;
1376     $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">';
1377 }
1378
1379 /** Configure default qweb */
1380 openerp.web._t = new openerp.web.TranslationDataBase().build_translation_function();
1381 /**
1382  * Lazy translation function, only performs the translation when actually
1383  * printed (e.g. inserted into a template)
1384  *
1385  * Useful when defining translatable strings in code evaluated before the
1386  * translation database is loaded, as class attributes or at the top-level of
1387  * an OpenERP Web module
1388  *
1389  * @param {String} s string to translate
1390  * @returns {Object} lazy translation object
1391  */
1392 openerp.web._lt = function (s) {
1393     return {toString: function () { return openerp.web._t(s); }}
1394 };
1395 openerp.web.qweb = new QWeb2.Engine();
1396 openerp.web.qweb.debug = ($.deparam($.param.querystring()).debug != undefined);
1397 openerp.web.qweb.default_dict = {
1398     '_' : _,
1399     '_t' : openerp.web._t
1400 };
1401 openerp.web.qweb.preprocess_node = function() {
1402     // Note that 'this' is the Qweb Node
1403     switch (this.node.nodeType) {
1404         case 3:
1405         case 4:
1406             // Text and CDATAs
1407             var translation = this.node.parentNode.attributes['t-translation'];
1408             if (translation && translation.value === 'off') {
1409                 return;
1410             }
1411             var ts = _.str.trim(this.node.data);
1412             if (ts.length === 0) {
1413                 return;
1414             }
1415             var tr = openerp.web._t(ts);
1416             if (tr !== ts) {
1417                 this.node.data = tr;
1418             }
1419             break;
1420         case 1:
1421             // Element
1422             var attr, attrs = ['label', 'title', 'alt'];
1423             while (attr = attrs.pop()) {
1424                 if (this.attributes[attr]) {
1425                     this.attributes[attr] = openerp.web._t(this.attributes[attr]);
1426                 }
1427             }
1428     }
1429 };
1430
1431 /**
1432  * A small utility function to check if a class implements correctly an interface, assuming that
1433  * interface is simply specified using a dictionary containing methods and attributes with the
1434  * correct type. It only performs the check when in debug mode and the only effect of an invalid
1435  * check is messages in the console.
1436  */
1437 openerp.web.check_interface = function(_class, _interface) {
1438     if (! openerp.web.check_interface.debug)
1439         return;
1440     for (var member in _interface) {
1441         if ( (typeof _class.prototype[member] != typeof _interface[member]) ) {
1442             console.error("class failed to implement interface member '" + member + "'");
1443         }
1444     }
1445 }
1446 openerp.web.check_interface.debug = ($.deparam($.param.querystring()).debug != undefined);
1447
1448 /** Jquery extentions */
1449 $.Mutex = (function() {
1450     function Mutex() {
1451         this.def = $.Deferred().resolve();
1452     }
1453     Mutex.prototype.exec = function(action) {
1454         var current = this.def;
1455         var next = this.def = $.Deferred();
1456         return current.pipe(function() {
1457             return $.when(action()).always(function() {
1458                 next.resolve();
1459             });
1460         });
1461     };
1462     return Mutex;
1463 })();
1464
1465 /** Setup default connection */
1466 openerp.connection = new openerp.web.Connection();
1467 openerp.web.qweb.default_dict['__debug__'] = openerp.connection.debug;
1468
1469
1470 $.async_when = function() {
1471     var async = false;
1472     var def = $.Deferred();
1473     $.when.apply($, arguments).then(function() {
1474         var args = arguments;
1475         var action = function() {
1476             def.resolve.apply(def, args);
1477         };
1478         if (async)
1479             action();
1480         else
1481             setTimeout(action, 0);
1482     }, function() {
1483         var args = arguments;
1484         var action = function() {
1485             def.reject.apply(def, args);
1486         };
1487         if (async)
1488             action();
1489         else
1490             setTimeout(action, 0);
1491     });
1492     async = true;
1493     return def;
1494 };
1495
1496 // special tweak for the web client
1497 var old_async_when = $.async_when;
1498 $.async_when = function() {
1499         if (openerp.connection.synch)
1500                 return $.when.apply(this, arguments);
1501         else
1502                 return old_async_when.apply(this, arguments);
1503 };
1504
1505 };
1506
1507 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: