1 /*---------------------------------------------------------
3 *--------------------------------------------------------*/
6 console = {log: function () {}};
9 console.debug = console.log;
12 openerp.web.coresetup = function(instance) {
14 /** Session openerp specific RPC class */
15 instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Session# */{
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();
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);
29 session_bind: function(origin) {
32 instance.web.qweb.default_dict['_s'] = this.origin;
33 this.session_id = false;
35 this.username = false;
36 this.user_context= {};
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;
45 this.active_id = null;
46 return this.session_init();
49 * Init a session, reloads from cookie, if it exists
51 session_init: function () {
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(); });
65 * (re)loads the content of a session: db name, username, user id, session
66 * context and status of the support contract
68 * @returns {$.Deferred} deferred indicating the session is done reloading
70 session_reload: function () {
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...)
77 session_id: result.session_id,
79 username: result.login,
81 user_context: result.context,
82 openerp_entreprise: result.openerp_entreprise
86 session_is_valid: function() {
90 * The session is validated either by login or by restoration of a previous session
92 session_authenticate: function(db, login, password, _volatile) {
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) {
98 return $.Deferred().reject();
102 session_id: result.session_id,
104 username: result.login,
106 user_context: result.context,
107 openerp_entreprise: result.openerp_entreprise
110 self.set_cookie('session_id', self.session_id);
112 return self.load_modules();
115 session_logout: function() {
116 this.set_cookie('session_id', '');
117 return this.rpc("/web/session/destroy", {});
119 on_session_valid: function() {
122 * Called when a rpc call fail due to an invalid session.
123 * By default, it's a noop
125 on_session_invalid: function(retry_callback) {
128 * Fetches a cookie stored by an openerp session
131 * @param name the cookie's name
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)));
146 * Create a new cookie with the provided name and value
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
153 set_cookie: function (name, value, ttl) {
154 if (!this.name) { return; }
155 ttl = ttl || 24*60*60*365;
157 this.name + '|' + name + '=' + encodeURIComponent(JSON.stringify(value)),
160 'expires=' + new Date(new Date().getTime() + ttl*1000).toGMTString()
164 * Load additional web addons of that instance and init them
166 * @param {Boolean} [no_session_valid_signal=false] prevents load_module from triggering ``on_session_valid``.
168 load_modules: function(no_session_valid_signal) {
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;
177 var loaded = $.Deferred().resolve().promise();
178 if (to_load.length) {
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';
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();
207 do_load_css: function (files) {
209 _.each(files, function (file) {
210 $('head').append($('<link>', {
211 'href': self.get_url(file),
217 do_load_js: function(files) {
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 )
228 tag.onload_done = true;
229 self.do_load_js(files).then(function () {
233 var head = document.head || document.getElementsByTagName('head')[0];
234 head.appendChild(tag);
240 do_load_qweb: function(files) {
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));
250 return self.qweb_mutex.def;
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])
259 if(instance._openerp[mod] != undefined) {
260 instance._openerp[mod](instance,instance[mod]);
261 this.module_loaded[mod] = true;
266 * Cooperative file download implementation, for ajaxy APIs.
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
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
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
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'),
296 var $form, $form_data = $('<div>');
298 var complete = function () {
299 if (options.complete) { options.complete(); }
303 if (remove_form && $form) { $form.remove(); }
305 var $target = $('<iframe style="display: none;">')
306 .attr({id: id, name: id})
307 .appendTo(document.body)
311 if (!this.contentDocument.body.childNodes[1]) {
312 options.error(this.contentDocument.body.childNodes);
315 options.error(JSON.parse(this.contentDocument.body.childNodes[1].textContent));
324 $form = $(options.form);
327 $form = $('<form>', {
330 }).appendTo(document.body);
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);
349 var waitLoop = function () {
350 var cookies = document.cookie.split(';');
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; }
360 document.cookie = _.str.sprintf("%s=;expires=%s;path=/",
361 cookie_name, new Date().toGMTString());
362 if (options.success) { options.success(); }
367 timer = setTimeout(waitLoop, CHECK_INTERVAL);
369 synchronized_mode: function(to_execute) {
370 var synch = this.synch;
382 * Event Bus used to bind events scoped in the current instance
384 instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin, {
386 instance.web.EventDispatcherMixin.init.call(this, parent);
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);
396 _.each('resize,scroll'.split(','), function(evtype) {
397 $(window).on(evtype, function(ev) {
398 self.trigger(evtype, ev);
403 instance.web.bus = new instance.web.Bus();
405 /** OpenERP Translations */
406 instance.web.TranslationDataBase = instance.web.Class.extend(/** @lends instance.web.TranslationDataBase# */{
408 * @constructs instance.web.TranslationDataBase
409 * @extends instance.web.Class
413 this.parameters = {"direction": 'ltr',
414 "date_format": '%m/%d/%Y',
415 "time_format": '%H:%M:%S',
417 "decimal_point": ".",
418 "thousands_sep": ","};
420 set_bundle: function(translation_bundle) {
423 var modules = _.keys(translation_bundle.modules);
425 if (_.include(modules, "web")) {
426 modules = ["web"].concat(_.without(modules, "web"));
428 _.each(modules, function(name) {
429 self.add_module_translation(translation_bundle.modules[name]);
431 if (translation_bundle.lang_parameters) {
432 this.parameters = translation_bundle.lang_parameters;
433 this.parameters.grouping = py.eval(
434 this.parameters.grouping);
437 add_module_translation: function(mod) {
439 _.each(mod.messages, function(message) {
440 self.db[message.id] = message.string;
443 build_translation_function: function() {
445 var fcnt = function(str) {
446 var tmp = self.get(str);
447 return tmp === undefined ? str : tmp;
449 fcnt.database = this;
459 /** Custom jQuery plugins */
460 $.fn.getAttributes = function() {
463 for (var attr, i = 0, attrs = this[0].attributes, l = attrs.length; i < l; i++) {
465 o[attr.nodeName] = attr.nodeValue;
471 /** Jquery extentions */
472 $.Mutex = (function() {
474 this.def = $.Deferred().resolve();
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() {
488 $.async_when = function() {
490 var def = $.Deferred();
491 $.when.apply($, arguments).then(function() {
492 var args = arguments;
493 var action = function() {
494 def.resolve.apply(def, args);
499 setTimeout(action, 0);
501 var args = arguments;
502 var action = function() {
503 def.reject.apply(def, args);
508 setTimeout(action, 0);
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);
520 return old_async_when.apply(this, arguments);
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"] = '';
531 var messages_by_seconds = [
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..."],
539 instance.web.Throbber = instance.web.Widget.extend({
540 template: "Throbber",
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
558 this.spin = new Spinner(opts).spin(this.$element[0]);
559 this.start_time = new Date().getTime();
562 act_message: function() {
564 setTimeout(function() {
565 if (self.isDestroyed())
567 var seconds = (new Date().getTime() - self.start_time) / 1000;
569 _.each(messages_by_seconds, function(el) {
570 if (seconds >= el[0])
573 self.$(".oe_throbber_message").html(mes);
577 destroy: function() {
583 instance.web.Throbber.throbbers = [];
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"));
592 instance.web.unblockUI = function() {
593 _.each(instance.web.Throbber.throbbers, function(el) {
596 return $.unblockUI.apply($, arguments);
599 /** Setup default session */
600 instance.session = new instance.web.Session();
602 /** Configure default qweb */
603 instance.web._t = new instance.web.TranslationDataBase().build_translation_function();
605 * Lazy translation function, only performs the translation when actually
606 * printed (e.g. inserted into a template)
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
612 * @param {String} s string to translate
613 * @returns {Object} lazy translation object
615 instance.web._lt = function (s) {
616 return {toString: function () { return instance.web._t(s); }}
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 = {
623 '_t' : instance.web._t
625 instance.web.qweb.preprocess_node = function() {
626 // Note that 'this' is the Qweb Node
627 switch (this.node.nodeType) {
631 var translation = this.node.parentNode.attributes['t-translation'];
632 if (translation && translation.value === 'off') {
635 var ts = _.str.trim(this.node.data);
636 if (ts.length === 0) {
639 var tr = instance.web._t(ts);
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]);
655 /** Setup jQuery timeago */
656 var _t = instance.web._t;
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
664 _t('less than a minute ago');
665 _t('about a minute ago');
666 _t('%d minutes ago');
667 _t('about an hour ago');
671 _t('about a month ago');
673 _t('about a year ago');
677 instance.session.on('module_loaded', this, function () {
678 // provide timeago.js with our own translator method
679 $.timeago.settings.translator = instance.web._t;
683 * Registry for all the client actions key: tag value: widget
685 instance.web.client_actions = new instance.web.Registry();
689 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: