Merged with latest.
[odoo/odoo.git] / addons / web / static / src / js / coresetup.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.coresetup = function(instance) {
13
14 /** Session openerp specific RPC class */
15 instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Session# */{
16     init: function() {
17         this._super.apply(this, arguments);
18         // TODO: session store in cookie should be optional
19         this.name = instance._session_id;
20         this.qweb_mutex = new $.Mutex();
21     },
22     rpc: function(url, params, success_callback, error_callback) {
23         params.session_id = this.session_id;
24         return this._super(url, params, success_callback, error_callback);
25     },
26     /**
27      * Setup a sessionm
28      */
29     session_bind: function(origin) {
30         var self = this;
31         this.setup(origin);
32         instance.web.qweb.default_dict['_s'] = this.origin;
33         this.session_id = false;
34         this.uid = false;
35         this.username = false;
36         this.user_context= {};
37         this.db = false;
38         this.openerp_entreprise = false;
39         this.module_list = instance._modules.slice();
40         this.module_loaded = {};
41         _(this.module_list).each(function (mod) {
42             self.module_loaded[mod] = true;
43         });
44         this.context = {};
45         this.active_id = null;
46         return this.session_init();
47     },
48     /**
49      * Init a session, reloads from cookie, if it exists
50      */
51     session_init: function () {
52         var self = this;
53         // TODO: session store in cookie should be optional
54         this.session_id = this.get_cookie('session_id');
55         return this.session_reload().pipe(function(result) {
56             var modules = instance._modules.join(',');
57             var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).pipe(self.do_load_qweb);
58             if(self.session_is_valid()) {
59                 return deferred.pipe(function() { return self.load_modules(); });
60             }
61             return deferred;
62         });
63     },
64     /**
65      * (re)loads the content of a session: db name, username, user id, session
66      * context and status of the support contract
67      *
68      * @returns {$.Deferred} deferred indicating the session is done reloading
69      */
70     session_reload: function () {
71         var self = this;
72         return this.rpc("/web/session/get_session_info", {}).then(function(result) {
73             // If immediately follows a login (triggered by trying to restore
74             // an invalid session or no session at all), refresh session data
75             // (should not change, but just in case...)
76             _.extend(self, {
77                 session_id: result.session_id,
78                 db: result.db,
79                 username: result.login,
80                 uid: result.uid,
81                 user_context: result.context,
82                 openerp_entreprise: result.openerp_entreprise
83             });
84         });
85     },
86     session_is_valid: function() {
87         return !!this.uid;
88     },
89     /**
90      * The session is validated either by login or by restoration of a previous session
91      */
92     session_authenticate: function(db, login, password, _volatile) {
93         var self = this;
94         var base_location = document.location.protocol + '//' + document.location.host;
95         var params = { db: db, login: login, password: password, base_location: base_location };
96         return this.rpc("/web/session/authenticate", params).pipe(function(result) {
97             if (!result.uid) {
98                 return $.Deferred().reject();
99             }
100
101             _.extend(self, {
102                 session_id: result.session_id,
103                 db: result.db,
104                 username: result.login,
105                 uid: result.uid,
106                 user_context: result.context,
107                 openerp_entreprise: result.openerp_entreprise
108             });
109             if (!_volatile) {
110                 self.set_cookie('session_id', self.session_id);
111             }
112             return self.load_modules();
113         });
114     },
115     session_logout: function() {
116         this.set_cookie('session_id', '');
117         return this.rpc("/web/session/destroy", {});
118     },
119     on_session_valid: function() {
120     },
121     /**
122      * Called when a rpc call fail due to an invalid session.
123      * By default, it's a noop
124      */
125     on_session_invalid: function(retry_callback) {
126     },
127     /**
128      * Fetches a cookie stored by an openerp session
129      *
130      * @private
131      * @param name the cookie's name
132      */
133     get_cookie: function (name) {
134         if (!this.name) { return null; }
135         var nameEQ = this.name + '|' + name + '=';
136         var cookies = document.cookie.split(';');
137         for(var i=0; i<cookies.length; ++i) {
138             var cookie = cookies[i].replace(/^\s*/, '');
139             if(cookie.indexOf(nameEQ) === 0) {
140                 return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
141             }
142         }
143         return null;
144     },
145     /**
146      * Create a new cookie with the provided name and value
147      *
148      * @private
149      * @param name the cookie's name
150      * @param value the cookie's value
151      * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete
152      */
153     set_cookie: function (name, value, ttl) {
154         if (!this.name) { return; }
155         ttl = ttl || 24*60*60*365;
156         document.cookie = [
157             this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
158             'path=/',
159             'max-age=' + ttl,
160             'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
161         ].join(';');
162     },
163     /**
164      * Load additional web addons of that instance and init them
165      *
166      * @param {Boolean} [no_session_valid_signal=false] prevents load_module from triggering ``on_session_valid``.
167      */
168     load_modules: function(no_session_valid_signal) {
169         var self = this;
170         return this.rpc('/web/session/modules', {}).pipe(function(result) {
171             var lang = self.user_context.lang,
172                 all_modules = _.uniq(self.module_list.concat(result));
173             var params = { mods: all_modules, lang: lang};
174             var to_load = _.difference(result, self.module_list).join(',');
175             self.module_list = all_modules;
176
177             var loaded = $.Deferred().resolve().promise();
178             if (to_load.length) {
179                 loaded = $.when(
180                     self.rpc('/web/webclient/csslist', {mods: to_load}, self.do_load_css),
181                     self.rpc('/web/webclient/qweblist', {mods: to_load}).pipe(self.do_load_qweb),
182                     self.rpc('/web/webclient/translations', params).pipe(function(trans) {
183                         instance.web._t.database.set_bundle(trans);
184                         var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"];
185                         return self.rpc('/web/webclient/jslist', {mods: to_load}).pipe(function(files) {
186                             return self.do_load_js(file_list.concat(files));
187                         }).then(function () {
188                             if (!Date.CultureInfo.pmDesignator) {
189                                 // If no am/pm designator is specified but the openerp
190                                 // datetime format uses %i, date.js won't be able to
191                                 // correctly format a date. See bug#938497.
192                                 Date.CultureInfo.amDesignator = 'AM';
193                                 Date.CultureInfo.pmDesignator = 'PM';
194                             }
195                         });
196                     }))
197             }
198             return loaded.then(function() {
199                 self.on_modules_loaded();
200                 self.trigger('module_loaded');
201                 if (!no_session_valid_signal) {
202                     self.on_session_valid();
203                 }
204             });
205         });
206     },
207     do_load_css: function (files) {
208         var self = this;
209         _.each(files, function (file) {
210             $('head').append($('<link>', {
211                 'href': self.get_url(file),
212                 'rel': 'stylesheet',
213                 'type': 'text/css'
214             }));
215         });
216     },
217     do_load_js: function(files) {
218         var self = this;
219         var d = $.Deferred();
220         if(files.length != 0) {
221             var file = files.shift();
222             var tag = document.createElement('script');
223             tag.type = 'text/javascript';
224             tag.src = self.get_url(file);
225             tag.onload = tag.onreadystatechange = function() {
226                 if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done )
227                     return;
228                 tag.onload_done = true;
229                 self.do_load_js(files).then(function () {
230                     d.resolve();
231                 });
232             };
233             var head = document.head || document.getElementsByTagName('head')[0];
234             head.appendChild(tag);
235         } else {
236             d.resolve();
237         }
238         return d;
239     },
240     do_load_qweb: function(files) {
241         var self = this;
242         _.each(files, function(file) {
243             self.qweb_mutex.exec(function() {
244                 return self.rpc('/web/proxy/load', {path: file}).pipe(function(xml) {
245                     if (!xml) { return; }
246                     instance.web.qweb.add_template(_.str.trim(xml));
247                 });
248             });
249         });
250         return self.qweb_mutex.def;
251     },
252     on_modules_loaded: function() {
253         for(var j=0; j<this.module_list.length; j++) {
254             var mod = this.module_list[j];
255             if(this.module_loaded[mod])
256                 continue;
257             instance[mod] = {};
258             // init module mod
259             if(instance._openerp[mod] != undefined) {
260                 instance._openerp[mod](instance,instance[mod]);
261                 this.module_loaded[mod] = true;
262             }
263         }
264     },
265     /**
266      * Cooperative file download implementation, for ajaxy APIs.
267      *
268      * Requires that the server side implements an httprequest correctly
269      * setting the `fileToken` cookie to the value provided as the `token`
270      * parameter. The cookie *must* be set on the `/` path and *must not* be
271      * `httpOnly`.
272      *
273      * It would probably also be a good idea for the response to use a
274      * `Content-Disposition: attachment` header, especially if the MIME is a
275      * "known" type (e.g. text/plain, or for some browsers application/json
276      *
277      * @param {Object} options
278      * @param {String} [options.url] used to dynamically create a form
279      * @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
280      * @param {HTMLFormElement} [options.form] the form to submit in order to fetch the file
281      * @param {Function} [options.success] callback in case of download success
282      * @param {Function} [options.error] callback in case of request error, provided with the error body
283      * @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed
284      */
285     get_file: function (options) {
286         // need to detect when the file is done downloading (not used
287         // yet, but we'll need it to fix the UI e.g. with a throbber
288         // while dump is being generated), iframe load event only fires
289         // when the iframe content loads, so we need to go smarter:
290         // http://geekswithblogs.net/GruffCode/archive/2010/10/28/detecting-the-file-download-dialog-in-the-browser.aspx
291         var timer, token = new Date().getTime(),
292             cookie_name = 'fileToken', cookie_length = cookie_name.length,
293             CHECK_INTERVAL = 1000, id = _.uniqueId('get_file_frame'),
294             remove_form = false;
295
296         var $form, $form_data = $('<div>');
297
298         var complete = function () {
299             if (options.complete) { options.complete(); }
300             clearTimeout(timer);
301             $form_data.remove();
302             $target.remove();
303             if (remove_form && $form) { $form.remove(); }
304         };
305         var $target = $('<iframe style="display: none;">')
306             .attr({id: id, name: id})
307             .appendTo(document.body)
308             .load(function () {
309                 try {
310                    if (options.error) {
311                          if (!this.contentDocument.body.childNodes[1]) {
312                             options.error(this.contentDocument.body.childNodes);
313                         }
314                         else {
315                             options.error(JSON.parse(this.contentDocument.body.childNodes[1].textContent));
316                         }
317                    }
318                 } finally {
319                     complete();
320                 }
321             });
322
323         if (options.form) {
324             $form = $(options.form);
325         } else {
326             remove_form = true;
327             $form = $('<form>', {
328                 action: options.url,
329                 method: 'POST'
330             }).appendTo(document.body);
331         }
332
333         _(_.extend({}, options.data || {},
334                    {session_id: this.session_id, token: token}))
335             .each(function (value, key) {
336                 var $input = $form.find('[name=' + key +']');
337                 if (!$input.length) {
338                     $input = $('<input type="hidden" name="' + key + '">')
339                         .appendTo($form_data);
340                 }
341                 $input.val(value)
342             });
343
344         $form
345             .append($form_data)
346             .attr('target', id)
347             .get(0).submit();
348
349         var waitLoop = function () {
350             var cookies = document.cookie.split(';');
351             // setup next check
352             timer = setTimeout(waitLoop, CHECK_INTERVAL);
353             for (var i=0; i<cookies.length; ++i) {
354                 var cookie = cookies[i].replace(/^\s*/, '');
355                 if (!cookie.indexOf(cookie_name === 0)) { continue; }
356                 var cookie_val = cookie.substring(cookie_length + 1);
357                 if (parseInt(cookie_val, 10) !== token) { continue; }
358
359                 // clear cookie
360                 document.cookie = _.str.sprintf("%s=;expires=%s;path=/",
361                     cookie_name, new Date().toGMTString());
362                 if (options.success) { options.success(); }
363                 complete();
364                 return;
365             }
366         };
367         timer = setTimeout(waitLoop, CHECK_INTERVAL);
368     },
369     synchronized_mode: function(to_execute) {
370         var synch = this.synch;
371         this.synch = true;
372         try {
373             return to_execute();
374         } finally {
375             this.synch = synch;
376         }
377     }
378 });
379
380
381 /**
382  * Event Bus used to bind events scoped in the current instance
383  */
384 instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin, {
385     init: function() {
386         instance.web.EventDispatcherMixin.init.call(this, parent);
387         var self = this;
388         // TODO fme: allow user to bind keys for some global actions.
389         //           check gtk bindings
390         // http://unixpapa.com/js/key.html
391         _.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) {
392             $('html').on(evtype, function(ev) {
393                 self.trigger(evtype, ev);
394             });
395         });
396         _.each('resize,scroll'.split(','), function(evtype) {
397             $(window).on(evtype, function(ev) {
398                 self.trigger(evtype, ev);
399             });
400         });
401     }
402 })
403 instance.web.bus = new instance.web.Bus();
404
405 /** OpenERP Translations */
406 instance.web.TranslationDataBase = instance.web.Class.extend(/** @lends instance.web.TranslationDataBase# */{
407     /**
408      * @constructs instance.web.TranslationDataBase
409      * @extends instance.web.Class
410      */
411     init: function() {
412         this.db = {};
413         this.parameters = {"direction": 'ltr',
414                         "date_format": '%m/%d/%Y',
415                         "time_format": '%H:%M:%S',
416                         "grouping": [],
417                         "decimal_point": ".",
418                         "thousands_sep": ","};
419     },
420     set_bundle: function(translation_bundle) {
421         var self = this;
422         this.db = {};
423         var modules = _.keys(translation_bundle.modules);
424         modules.sort();
425         if (_.include(modules, "web")) {
426             modules = ["web"].concat(_.without(modules, "web"));
427         }
428         _.each(modules, function(name) {
429             self.add_module_translation(translation_bundle.modules[name]);
430         });
431         if (translation_bundle.lang_parameters) {
432             this.parameters = translation_bundle.lang_parameters;
433             this.parameters.grouping = py.eval(
434                     this.parameters.grouping);
435         }
436     },
437     add_module_translation: function(mod) {
438         var self = this;
439         _.each(mod.messages, function(message) {
440             self.db[message.id] = message.string;
441         });
442     },
443     build_translation_function: function() {
444         var self = this;
445         var fcnt = function(str) {
446             var tmp = self.get(str);
447             return tmp === undefined ? str : tmp;
448         };
449         fcnt.database = this;
450         return fcnt;
451     },
452     get: function(key) {
453         if (this.db[key])
454             return this.db[key];
455         return undefined;
456     }
457 });
458
459 /** Custom jQuery plugins */
460 $.fn.getAttributes = function() {
461     var o = {};
462     if (this.length) {
463         for (var attr, i = 0, attrs = this[0].attributes, l = attrs.length; i < l; i++) {
464             attr = attrs.item(i)
465             o[attr.nodeName] = attr.nodeValue;
466         }
467     }
468     return o;
469 }
470
471 /** Jquery extentions */
472 $.Mutex = (function() {
473     function Mutex() {
474         this.def = $.Deferred().resolve();
475     }
476     Mutex.prototype.exec = function(action) {
477         var current = this.def;
478         var next = this.def = $.Deferred();
479         return current.pipe(function() {
480             return $.when(action()).always(function() {
481                 next.resolve();
482             });
483         });
484     };
485     return Mutex;
486 })();
487
488 $.async_when = function() {
489     var async = false;
490     var def = $.Deferred();
491     $.when.apply($, arguments).then(function() {
492         var args = arguments;
493         var action = function() {
494             def.resolve.apply(def, args);
495         };
496         if (async)
497             action();
498         else
499             setTimeout(action, 0);
500     }, function() {
501         var args = arguments;
502         var action = function() {
503             def.reject.apply(def, args);
504         };
505         if (async)
506             action();
507         else
508             setTimeout(action, 0);
509     });
510     async = true;
511     return def;
512 };
513
514 // special tweak for the web client
515 var old_async_when = $.async_when;
516 $.async_when = function() {
517     if (instance.session.synch)
518         return $.when.apply(this, arguments);
519     else
520         return old_async_when.apply(this, arguments);
521 };
522
523 /** Setup blockui */
524 if ($.blockUI) {
525     $.blockUI.defaults.baseZ = 1100;
526     $.blockUI.defaults.message = '<div class="oe_blockui_spin_container">';
527     $.blockUI.defaults.css.border = '0';
528     $.blockUI.defaults.css["background-color"] = '';
529 }
530
531 var messages_by_seconds = [
532     [0, "Loading..."],
533     [30, "Still Loading..."],
534     [60, "Still Loading...<br />Please be patient."],
535     [120, "Hey, guess what?<br />It's still loading."],
536     [300, "You may not believe it,<br/>but the application is actually loading..."],
537 ];
538
539 instance.web.Throbber = instance.web.Widget.extend({
540     template: "Throbber",
541     start: function() {
542         var opts = {
543           lines: 13, // The number of lines to draw
544           length: 7, // The length of each line
545           width: 4, // The line thickness
546           radius: 10, // The radius of the inner circle
547           rotate: 0, // The rotation offset
548           color: '#FFF', // #rgb or #rrggbb
549           speed: 1, // Rounds per second
550           trail: 60, // Afterglow percentage
551           shadow: false, // Whether to render a shadow
552           hwaccel: false, // Whether to use hardware acceleration
553           className: 'spinner', // The CSS class to assign to the spinner
554           zIndex: 2e9, // The z-index (defaults to 2000000000)
555           top: 'auto', // Top position relative to parent in px
556           left: 'auto' // Left position relative to parent in px
557         };
558         this.spin = new Spinner(opts).spin(this.$element[0]);
559         this.start_time = new Date().getTime();
560         this.act_message();
561     },
562     act_message: function() {
563         var self = this;
564         setTimeout(function() {
565             if (self.isDestroyed())
566                 return;
567             var seconds = (new Date().getTime() - self.start_time) / 1000;
568             var mes;
569             _.each(messages_by_seconds, function(el) {
570                 if (seconds >= el[0])
571                     mes = el[1];
572             });
573             self.$(".oe_throbber_message").html(mes);
574             self.act_message();
575         }, 1000);
576     },
577     destroy: function() {
578         if (this.spin)
579             this.spin.stop();
580         this._super();
581     },
582 });
583 instance.web.Throbber.throbbers = [];
584
585 instance.web.blockUI = function() {
586     var tmp = $.blockUI.apply($, arguments);
587     var throbber = new instance.web.Throbber();
588     instance.web.Throbber.throbbers.push(throbber);
589     throbber.appendTo($(".oe_blockui_spin_container"));
590     return tmp;
591 }
592 instance.web.unblockUI = function() {
593     _.each(instance.web.Throbber.throbbers, function(el) {
594         el.destroy();
595     });
596     return $.unblockUI.apply($, arguments);
597 }
598
599 /** Setup default session */
600 instance.session = new instance.web.Session();
601
602 /** Configure default qweb */
603 instance.web._t = new instance.web.TranslationDataBase().build_translation_function();
604 /**
605  * Lazy translation function, only performs the translation when actually
606  * printed (e.g. inserted into a template)
607  *
608  * Useful when defining translatable strings in code evaluated before the
609  * translation database is loaded, as class attributes or at the top-level of
610  * an OpenERP Web module
611  *
612  * @param {String} s string to translate
613  * @returns {Object} lazy translation object
614  */
615 instance.web._lt = function (s) {
616     return {toString: function () { return instance.web._t(s); }}
617 };
618 instance.web.qweb = new QWeb2.Engine();
619 instance.web.qweb.default_dict['__debug__'] = instance.session.debug; // Which one ?
620 instance.web.qweb.debug = instance.session.debug;
621 instance.web.qweb.default_dict = {
622     '_' : _,
623     '_t' : instance.web._t
624 };
625 instance.web.qweb.preprocess_node = function() {
626     // Note that 'this' is the Qweb Node
627     switch (this.node.nodeType) {
628         case 3:
629         case 4:
630             // Text and CDATAs
631             var translation = this.node.parentNode.attributes['t-translation'];
632             if (translation && translation.value === 'off') {
633                 return;
634             }
635             var ts = _.str.trim(this.node.data);
636             if (ts.length === 0) {
637                 return;
638             }
639             var tr = instance.web._t(ts);
640             if (tr !== ts) {
641                 this.node.data = tr;
642             }
643             break;
644         case 1:
645             // Element
646             var attr, attrs = ['label', 'title', 'alt', 'placeholder'];
647             while (attr = attrs.pop()) {
648                 if (this.attributes[attr]) {
649                     this.attributes[attr] = instance.web._t(this.attributes[attr]);
650                 }
651             }
652     }
653 };
654
655 /** Setup jQuery timeago */
656 var _t = instance.web._t;
657 /*
658  * Strings in timeago are "composed" with prefixes, words and suffixes. This
659  * makes their detection by our translating system impossible. Use all literal
660  * strings we're using with a translation mark here so the extractor can do its
661  * job.
662  */
663 {
664     _t('less than a minute ago');
665     _t('about a minute ago');
666     _t('%d minutes ago');
667     _t('about an hour ago');
668     _t('%d hours ago');
669     _t('a day ago');
670     _t('%d days ago');
671     _t('about a month ago');
672     _t('%d months ago');
673     _t('about a year ago');
674     _t('%d years ago');
675 }
676
677 instance.session.on('module_loaded', this, function () {
678     // provide timeago.js with our own translator method
679     $.timeago.settings.translator = instance.web._t;
680 });
681
682 /**
683  * Registry for all the client actions key: tag value: widget
684  */
685 instance.web.client_actions = new instance.web.Registry();
686
687 };
688
689 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: