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