db3cb69519f02aaabdbcad8984021bca57edc7f4
[odoo/odoo.git] / addons / web / static / src / js / chrome.js
1 /*---------------------------------------------------------
2  * OpenERP Web chrome
3  *---------------------------------------------------------*/
4 (function() {
5
6 var instance = openerp;
7 openerp.web.chrome = {};
8
9 var QWeb = instance.web.qweb,
10     _t = instance.web._t;
11
12 instance.web.Notification =  instance.web.Widget.extend({
13     template: 'Notification',
14     init: function() {
15         this._super.apply(this, arguments);
16         instance.web.notification = this;
17     },
18     start: function() {
19         this._super.apply(this, arguments);
20         this.$el.notify({
21             speed: 500,
22             expires: 2500
23         });
24     },
25     notify: function(title, text, sticky) {
26         sticky = !!sticky;
27         var opts = {};
28         if (sticky) {
29             opts.expires = false;
30         }
31         return this.$el.notify('create', {
32             title: title,
33             text: text
34         }, opts);
35     },
36     warn: function(title, text, sticky) {
37         sticky = !!sticky;
38         var opts = {};
39         if (sticky) {
40             opts.expires = false;
41         }
42         return this.$el.notify('create', 'oe_notification_alert', {
43             title: title,
44             text: text
45         }, opts);
46     }
47 });
48
49 instance.web.action_notify = function(element, action) {
50     element.do_notify(action.params.title, action.params.text, action.params.sticky);
51 };
52 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
53
54 instance.web.action_warn = function(element, action) {
55     element.do_warn(action.params.title, action.params.text, action.params.sticky);
56 };
57 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
58
59 /**
60  * The very minimal function everything should call to create a dialog
61  * in OpenERP Web Client.
62  */
63 instance.web.dialog = function(element) {
64     var result = element.dialog.apply(element, _.rest(_.toArray(arguments)));
65     result.dialog("widget").openerpClass();
66     return result;
67 };
68
69 /**
70     A useful class to handle dialogs.
71
72     Attributes:
73     - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
74     during the lifecycle of the dialog.
75 */
76 instance.web.Dialog = instance.web.Widget.extend({
77     dialog_title: "",
78     /**
79         Constructor.
80
81         @param {Widget} parent
82         @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
83             dictionary can contain the following keys:
84             - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
85                 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
86                 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
87                 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
88                 feature does not allow that kind of personalization.
89             - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
90         @param {jQuery object} content Some content to replace this.$el .
91     */
92     init: function (parent, options, content) {
93         var self = this;
94         this._super(parent);
95         this.content_to_set = content;
96         this.dialog_options = {
97             modal: true,
98             destroy_on_close: true,
99             width: 900,
100             min_width: 0,
101             max_width: '95%',
102             height: 'auto',
103             min_height: 0,
104             max_height: $(window.top).height() - 200,
105             autoOpen: false,
106             position: [false, 40],
107             buttons: null,
108             beforeClose: function () {
109                 self.trigger("closing");
110             },
111             resizeStop: function() {
112                 self.trigger("resized");
113             },
114         };
115         if (options) {
116             _.extend(this.dialog_options, options);
117         }
118         this.on("closing", this, this._closing);
119         this.$buttons = $('<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"><span class="oe_dialog_custom_buttons"/></div>');
120     },
121     _get_options: function() {
122         var self = this;
123         var o = _.extend({}, this.dialog_options);
124         var sizes = {
125             width: $(window.top).width(),
126             height: $(window.top).height(),
127         };
128         _.each(sizes, function(available_size, unit) {
129             o[unit] = self._get_size(o[unit], available_size);
130             o['min_' + unit] = self._get_size(o['min_' + unit] || 0, available_size);
131             o['max_' + unit] = self._get_size(o['max_' + unit] || 0, available_size);
132             if (o[unit] !== 'auto' && o['min_' + unit] && o[unit] < o['min_' + unit]) {
133                 o[unit] = o['min_' + unit];
134             }
135             if (o[unit] !== 'auto' && o['max_' + unit] && o[unit] > o['max_' + unit]) {
136                 o[unit] = o['max_' + unit];
137             }
138         });
139         o.title = o.title || this.dialog_title;
140         return o;
141     },
142     _get_size: function(val, available_size) {
143         val = val.toString();
144         if (val === 'auto') {
145             return val;
146         } else if (val.slice(-1) === "%") {
147             return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10));
148         } else {
149             return parseInt(val, 10);
150         }
151     },
152     renderElement: function() {
153         if (this.content_to_set) {
154             this.setElement(this.content_to_set);
155         } else if (this.template) {
156             this._super();
157         }
158     },
159     /**
160         Opens the popup. Inits the dialog if it is not already inited.
161
162         @return this
163     */
164     open: function() {
165         if (!this.dialog_inited) {
166             this.init_dialog();
167         }
168         this.$el.dialog('open');
169         this.$el.dialog("widget").append(this.$buttons);
170         return this;
171     },
172     _add_buttons: function(buttons) {
173         var self = this;
174         var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
175         _.each(buttons, function(fn, text) {
176             // buttons can be object or array
177             if (!_.isFunction(fn)) {
178                 text = fn.text;
179                 fn = fn.click;
180             }
181             var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
182             $customButons.append($but);
183             $but.on('click', function(ev) {
184                 fn.call(self.$el, ev);
185             });
186         });
187     },
188     /**
189         Initializes the popup.
190
191         @return The result returned by start().
192     */
193     init_dialog: function() {
194         var options = this._get_options();
195         if (options.buttons) {
196             this._add_buttons(options.buttons);
197             delete(options.buttons);
198         }
199         this.renderElement();
200         instance.web.dialog(this.$el, options);
201         if (options.height === 'auto' && options.max_height) {
202             this.$el.css({ 'max-height': options.max_height, 'overflow-y': 'auto' });
203         }
204         this.dialog_inited = true;
205         var res = this.start();
206         return res;
207     },
208     /**
209         Closes the popup, if destroy_on_close was passed to the constructor, it is also destroyed.
210     */
211     close: function() {
212         if (this.dialog_inited && this.$el.is(":data(dialog)")) {
213             this.$el.dialog('close');
214         }
215     },
216     _closing: function() {
217         if (this.__tmp_dialog_destroying)
218             return;
219         if (this.dialog_options.destroy_on_close) {
220             this.__tmp_dialog_closing = true;
221             this.destroy();
222             this.__tmp_dialog_closing = undefined;
223         }
224     },
225     /**
226         Destroys the popup, also closes it.
227     */
228     destroy: function () {
229         this.$buttons.remove();
230         _.each(this.getChildren(), function(el) {
231             el.destroy();
232         });
233         if (! this.__tmp_dialog_closing) {
234             this.__tmp_dialog_destroying = true;
235             this.close();
236             this.__tmp_dialog_destroying = undefined;
237         }
238         if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(dialog)")) {
239             this.$el.dialog('destroy');
240         }
241         this._super();
242     }
243 });
244
245 instance.web.CrashManager = instance.web.Class.extend({
246     init: function() {
247         this.active = true;
248     },
249
250     rpc_error: function(error) {
251         if (!this.active) {
252             return;
253         }
254         var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
255         if (handler) {
256             new (handler)(this, error).display();
257             return;
258         }
259         if (error.data.name === "openerp.addons.web.session SessionExpiredException") {
260             this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
261             return;
262         }
263         if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning"
264                 || error.data.exception_type === "access_error") {
265             this.show_warning(error);
266         } else {
267             this.show_error(error);
268         }
269     },
270     show_warning: function(error) {
271         if (!this.active) {
272             return;
273         }
274         if (error.data.exception_type === "except_osv") {
275             error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
276         }
277         instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
278             title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
279             buttons: [
280                 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
281             ]
282         });
283     },
284     show_error: function(error) {
285         if (!this.active) {
286             return;
287         }
288         var buttons = {};
289         buttons[_t("Ok")] = function() {
290             $(this).dialog("close");
291         };
292         var dialog = new instance.web.Dialog(this, {
293             title: "OpenERP " + _.str.capitalize(error.type),
294             width: '80%',
295             height: '50%',
296             min_width: '800px',
297             min_height: '600px',
298             buttons: buttons
299         }).open();
300         dialog.$el.html(QWeb.render('CrashManager.error', {session: instance.session, error: error}));
301     },
302     show_message: function(exception) {
303         this.show_error({
304             type: _t("Client Error"),
305             message: exception,
306             data: {debug: ""}
307         });
308     },
309 });
310
311 /**
312     An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
313 */
314 instance.web.ExceptionHandler = {
315     /**
316         @param parent The parent.
317         @param error The error object as returned by the JSON-RPC implementation.
318     */
319     init: function(parent, error) {},
320     /**
321         Called to inform to display the widget, if necessary. A typical way would be to implement
322         this interface in a class extending instance.web.Dialog and simply display the dialog in this
323         method.
324     */
325     display: function() {},
326 };
327
328 /**
329     The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
330     instance.web.ExceptionHandler.
331 */
332 instance.web.crash_manager_registry = new instance.web.Registry();
333
334 /**
335  * Handle redirection warnings, which behave more or less like a regular
336  * warning, with an additional redirection button.
337  */
338 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
339     init: function(parent, error) {
340         this._super(parent);
341         this.error = error;
342     },
343     display: function() {
344         error = this.error;
345         error.data.message = error.data.arguments[0];
346
347         instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
348             title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
349             buttons: [
350                 {text: _t("Ok"), click: function() { $(this).dialog("close"); }},
351                 {text: error.data.arguments[2], click: function() {
352                     window.location.href='#action='+error.data.arguments[1];
353                     $(this).dialog("close");
354                 }}
355             ]
356         });
357         this.destroy();
358     }
359 });
360 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
361
362 instance.web.Loading = instance.web.Widget.extend({
363     template: _t("Loading"),
364     init: function(parent) {
365         this._super(parent);
366         this.count = 0;
367         this.blocked_ui = false;
368         this.session.on("request", this, this.request_call);
369         this.session.on("response", this, this.response_call);
370         this.session.on("response_failed", this, this.response_call);
371     },
372     destroy: function() {
373         this.on_rpc_event(-this.count);
374         this._super();
375     },
376     request_call: function() {
377         this.on_rpc_event(1);
378     },
379     response_call: function() {
380         this.on_rpc_event(-1);
381     },
382     on_rpc_event : function(increment) {
383         var self = this;
384         if (!this.count && increment === 1) {
385             // Block UI after 3s
386             this.long_running_timer = setTimeout(function () {
387                 self.blocked_ui = true;
388                 instance.web.blockUI();
389             }, 3000);
390         }
391
392         this.count += increment;
393         if (this.count > 0) {
394             if (instance.session.debug) {
395                 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
396             } else {
397                 this.$el.text(_t("Loading"));
398             }
399             this.$el.show();
400             this.getParent().$el.addClass('oe_wait');
401         } else {
402             this.count = 0;
403             clearTimeout(this.long_running_timer);
404             // Don't unblock if blocked by somebody else
405             if (self.blocked_ui) {
406                 this.blocked_ui = false;
407                 instance.web.unblockUI();
408             }
409             this.$el.fadeOut();
410             this.getParent().$el.removeClass('oe_wait');
411         }
412     }
413 });
414
415 instance.web.DatabaseManager = instance.web.Widget.extend({
416     init: function(parent) {
417         this._super(parent);
418         this.unblockUIFunction = instance.web.unblockUI;
419         $.validator.addMethod('matches', function (s, _, re) {
420             return new RegExp(re).test(s);
421         }, _t("Invalid database name"));
422     },
423     start: function() {
424         var self = this;
425         $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
426         var fetch_db = this.rpc("/web/database/get_list", {}).then(
427             function(result) {
428                 self.db_list = result;
429             },
430             function (_, ev) {
431                 ev.preventDefault();
432                 self.db_list = null;
433             });
434         var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
435             self.lang_list = result;
436         });
437         return $.when(fetch_db, fetch_langs).always(self.do_render);
438     },
439     do_render: function() {
440         var self = this;
441         instance.webclient.toggle_bars(true);
442         self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
443         $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
444         $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
445         $('ul.oe_secondary_submenu > li:first').addClass('oe_active');
446         $('ul.oe_secondary_submenu > li').bind('click', function (event) {
447             var menuitem = $(this);
448             menuitem.addClass('oe_active').siblings().removeClass('oe_active');
449             var form_id =menuitem.find('a').attr('href');
450             $(form_id).show().siblings().hide();
451             event.preventDefault();
452         });
453         $('#back-to-login').click(self.do_exit);
454         self.$el.find("td").addClass("oe_form_group_cell");
455         self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
456         self.$el.find("label").addClass("oe_form_label");
457         self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
458         self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
459         self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
460         self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
461         self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
462         self.$el.find("form[name=change_pwd_form]").validate({
463             messages: {
464                 old_pwd: _t("Please enter your previous password"),
465                 new_pwd: _t("Please enter your new password"),
466                 confirm_pwd: {
467                     required: _t("Please confirm your new password"),
468                     equalTo: _t("The confirmation does not match the password")
469                 }
470             },
471             submitHandler: self.do_change_password
472         });
473     },
474     destroy: function () {
475         this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
476         this._super();
477     },
478     /**
479      * Converts a .serializeArray() result into a dict. Does not bother folding
480      * multiple identical keys into an array, last key wins.
481      *
482      * @param {Array} array
483      */
484     to_object: function (array) {
485         var result = {};
486         _(array).each(function (record) {
487             result[record.name] = record.value;
488         });
489         return result;
490     },
491     /**
492      * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
493      * from unblocking the UI
494      */
495     blockUI: function () {
496         instance.web.blockUI();
497         instance.web.unblockUI = function () {};
498     },
499     /**
500      * Reinstates $.unblockUI so third parties can play with blockUI, and
501      * unblocks the UI
502      */
503     unblockUI: function () {
504         instance.web.unblockUI = this.unblockUIFunction;
505         instance.web.unblockUI();
506     },
507     /**
508      * Displays an error dialog resulting from the various RPC communications
509      * failing over themselves
510      *
511      * @param {Object} error error description
512      * @param {String} error.title title of the error dialog
513      * @param {String} error.error message of the error dialog
514      */
515     display_error: function (error) {
516         return instance.web.dialog($('<div>'), {
517             modal: true,
518             title: error.title,
519             buttons: [
520                 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
521             ]
522         }).html(error.error);
523     },
524     do_create: function(form) {
525         var self = this;
526         var fields = $(form).serializeArray();
527         self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
528             var form_obj = self.to_object(fields);
529             var client_action = {
530                 type: 'ir.actions.client',
531                 tag: 'login',
532                 params: {
533                     'db': form_obj['db_name'],
534                     'login': 'admin',
535                     'password': form_obj['create_admin_pwd'],
536                     'login_successful': function() {
537                         var url = '/?db=' + form_obj['db_name'];
538                         if (self.session.debug) {
539                             url += '&debug';
540                         }
541                         instance.web.redirect(url);
542                     },
543                 },
544                 _push_me: false,
545             };
546             self.do_action(client_action);
547         });
548     },
549     do_duplicate: function(form) {
550         var self = this;
551         var fields = $(form).serializeArray();
552         self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
553             if (result.error) {
554                 self.display_error(result);
555                 return;
556             }
557             self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
558             self.start();
559         });
560     },
561     do_drop: function(form) {
562         var self = this;
563         var $form = $(form),
564             fields = $form.serializeArray(),
565             $db_list = $form.find('[name=drop_db]'),
566             db = $db_list.val();
567         if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
568             return;
569         }
570         self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
571             if (result.error) {
572                 self.display_error(result);
573                 return;
574             }
575             self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
576             self.start();
577         });
578     },
579     do_backup: function(form) {
580         var self = this;
581         self.blockUI();
582         self.session.get_file({
583             form: form,
584             success: function () {
585                 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
586             },
587             error: function(error){
588                if(error){
589                   self.display_error({
590                         title: _t("Backup Database"),
591                         error: 'AccessDenied'
592                   });
593                }
594             },
595             complete: function() {
596                 self.unblockUI();
597             }
598         });
599     },
600     do_restore: function(form) {
601         var self = this;
602         self.blockUI();
603         $(form).ajaxSubmit({
604             url: '/web/database/restore',
605             type: 'POST',
606             resetForm: true,
607             success: function (body) {
608                 // If empty body, everything went fine
609                 if (!body) { return; }
610
611                 if (body.indexOf('403 Forbidden') !== -1) {
612                     self.display_error({
613                         title: _t("Access Denied"),
614                         error: _t("Incorrect super-administrator password")
615                     });
616                 } else {
617                     self.display_error({
618                         title: _t("Restore Database"),
619                         error: _t("Could not restore the database")
620                     });
621                 }
622             },
623             complete: function() {
624                 self.unblockUI();
625                 self.do_notify(_t("Restored"), _t("Database restored successfully"));
626             }
627         });
628     },
629     do_change_password: function(form) {
630         var self = this;
631         self.rpc("/web/database/change_password", {
632             'fields': $(form).serializeArray()
633         }).done(function(result) {
634             if (result.error) {
635                 self.display_error(result);
636                 return;
637             }
638             self.unblockUI();
639             self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
640         });
641     },
642     do_exit: function () {
643         this.$el.remove();
644         instance.webclient.show_login();
645     }
646 });
647 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
648
649 instance.web.Login =  instance.web.Widget.extend({
650     template: "Login",
651     remember_credentials: true,
652     events: {
653         'change input[name=db],select[name=db]': function(ev) {
654             this.set('database_selector', $(ev.currentTarget).val());
655         },
656     },
657
658     init: function(parent, action) {
659         this._super(parent);
660         this.has_local_storage = typeof(localStorage) != 'undefined';
661         this.db_list = null;
662         this.selected_db = null;
663         this.selected_login = null;
664         this.params = action.params || {};
665         if (_.isEmpty(this.params)) {
666             this.params = $.bbq.getState(true);
667         }
668         if (action && action.params && action.params.db) {
669             this.params.db = action.params.db;
670         } else if ($.deparam.querystring().db) {
671             this.params.db = $.deparam.querystring().db;
672         }
673         if (this.params.db) {
674             this.selected_db = this.params.db;
675         }
676
677         if (this.params.login_successful) {
678             this.on('login_successful', this, this.params.login_successful);
679         }
680     },
681     start: function() {
682         var self = this;
683         self.$el.find("form").submit(self.on_submit);
684         self.$el.find('.oe_login_manage_db').click(function() {
685             self.do_action("database_manager");
686         });
687         self.on('change:database_selector', this, function() {
688             this.database_selected(this.get('database_selector'));
689         });
690         var d = $.when();
691         if ($.param.fragment().token) {
692             self.params.token = $.param.fragment().token;
693         }
694         // used by dbmanager.do_create via internal client action
695         if (self.params.db && self.params.login && self.params.password) {
696             d = self.do_login(self.params.db, self.params.login, self.params.password);
697         } else {
698             d = self.rpc("/web/database/get_list", {})
699                 .done(self.on_db_loaded)
700                 .fail(self.on_db_failed)
701                 .always(function() {
702                     if (self.selected_db && self.has_local_storage && self.remember_credentials) {
703                         self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
704                         if (self.session.debug) {
705                             self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || '');
706                         }
707                     }
708                 });
709         }
710         return d;
711     },
712     database_selected: function(db) {
713         var params = $.deparam.querystring();
714         params.db = db;
715         this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
716         this.$('[name=login], [name=password]').prop('readonly', true);
717         instance.web.redirect('/?' + $.param(params));
718     },
719     on_db_loaded: function (result) {
720         var self = this;
721         this.db_list = result;
722         if (!this.selected_db) {
723             this.selected_db = result[0];
724         }
725         this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
726         if(this.db_list.length === 0) {
727             this.do_action("database_manager");
728         } else if(this.db_list.length === 1) {
729             this.$('div.oe_login_dbpane').hide();
730         } else {
731             this.$('div.oe_login_dbpane').show();
732         }
733     },
734     on_db_failed: function (error, event) {
735         if (error.data.name === 'openerp.exceptions.AccessDenied') {
736             event.preventDefault();
737         }
738     },
739     on_submit: function(ev) {
740         if(ev) {
741             ev.preventDefault();
742         }
743         var db = this.$("form [name=db]").val();
744         if (!db) {
745             this.do_warn(_t("Login"), _t("No database selected !"));
746             return false;
747         }
748         var login = this.$("form input[name=login]").val();
749         var password = this.$("form input[name=password]").val();
750
751         this.do_login(db, login, password);
752     },
753     /**
754      * Performs actual login operation, and UI-related stuff
755      *
756      * @param {String} db database to log in
757      * @param {String} login user login
758      * @param {String} password user password
759      */
760     do_login: function (db, login, password) {
761         var self = this;
762         self.hide_error();
763         self.$(".oe_login_pane").fadeOut("slow");
764         return this.session.session_authenticate(db, login, password).then(function() {
765             if (self.has_local_storage && self.remember_credentials) {
766                 localStorage.setItem(db + '|last_login', login);
767                 if (self.session.debug) {
768                     localStorage.setItem(db + '|last_password', password);
769                 }
770             }
771             self.trigger('login_successful');
772         }, function () {
773             self.$(".oe_login_pane").fadeIn("fast", function() {
774                 self.show_error(_t("Invalid username or password"));
775             });
776         });
777     },
778     show_error: function(message) {
779         this.$el.addClass("oe_login_invalid");
780         this.$(".oe_login_error_message").text(message);
781     },
782     hide_error: function() {
783         this.$el.removeClass('oe_login_invalid');
784     },
785 });
786 instance.web.client_actions.add("login", "instance.web.Login");
787
788
789 /**
790  * Redirect to url by replacing window.location
791  * If wait is true, sleep 1s and wait for the server i.e. after a restart.
792  */
793 instance.web.redirect = function(url, wait) {
794     // Dont display a dialog if some xmlhttprequest are in progress
795     if (instance.client && instance.client.crashmanager) {
796         instance.client.crashmanager.active = false;
797     }
798
799     var load = function() {
800         var old = "" + window.location;
801         if (old === url) {
802             window.location.reload();
803         } else {
804             window.location = url;
805         }
806     };
807
808     var wait_server = function() {
809         instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
810             setTimeout(wait_server, 250);
811         });
812     };
813
814     if (wait) {
815         setTimeout(wait_server, 1000);
816     } else {
817         load();
818     }
819 };
820
821 /**
822  * Client action to reload the whole interface.
823  * If params.menu_id, it opens the given menu entry.
824  * If params.wait, reload will wait the openerp server to be reachable before reloading
825  */
826 instance.web.Reload = function(parent, action) {
827     var params = action.params || {};
828     var menu_id = params.menu_id || false;
829     var l = window.location;
830
831     var sobj = $.deparam(l.search.substr(1));
832     if (params.url_search) {
833         sobj = _.extend(sobj, params.url_search);
834     }
835     var search = '?' + $.param(sobj);
836
837     var hash = l.hash;
838     if (menu_id) {
839         hash = "#menu_id=" + menu_id;
840     }
841     var url = l.protocol + "//" + l.host + l.pathname + search + hash;
842
843     instance.web.redirect(url, params.wait);
844 };
845 instance.web.client_actions.add("reload", "instance.web.Reload");
846
847 /**
848  * Client action to go back in breadcrumb history.
849  * If can't go back in history stack, will go back to home.
850  */
851 instance.web.HistoryBack = function(parent) {
852     if (!parent.history_back()) {
853         instance.web.Home(parent);
854     }
855 };
856 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
857
858 /**
859  * Client action to go back home.
860  */
861 instance.web.Home = function(parent, action) {
862     var url = '/' + (window.location.search || '');
863     instance.web.redirect(url, action.params && action.params.wait);
864 };
865 instance.web.client_actions.add("home", "instance.web.Home");
866
867 instance.web.ChangePassword =  instance.web.Widget.extend({
868     template: "ChangePassword",
869     start: function() {
870         var self = this;
871         this.getParent().dialog_title = _t("Change Password");
872         var $button = self.$el.find('.oe_form_button');
873         $button.appendTo(this.getParent().$buttons);
874         $button.eq(2).click(function(){
875            self.getParent().close();
876         });
877         $button.eq(0).click(function(){
878           self.rpc("/web/session/change_password",{
879                'fields': $("form[name=change_password_form]").serializeArray()
880           }).done(function(result) {
881                if (result.error) {
882                   self.display_error(result);
883                   return;
884                } else {
885                    instance.webclient.on_logout();
886                }
887           });
888        });
889     },
890     display_error: function (error) {
891         return instance.web.dialog($('<div>'), {
892             modal: true,
893             title: error.title,
894             buttons: [
895                 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
896             ]
897         }).html(error.error);
898     },
899 });
900 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
901
902 instance.web.Menu =  instance.web.Widget.extend({
903     template: 'Menu',
904     init: function() {
905         var self = this;
906         this._super.apply(this, arguments);
907         this.has_been_loaded = $.Deferred();
908         this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
909         this.data = {data:{children:[]}};
910         this.on("menu_loaded", this, function (menu_data) {
911             self.reflow();
912             // launch the fetch of needaction counters, asynchronous
913             if (!_.isEmpty(menu_data.all_menu_ids)) {
914                 this.do_load_needaction(menu_data.all_menu_ids);
915             }
916         });
917         var lazyreflow = _.debounce(this.reflow.bind(this), 200);
918         instance.web.bus.on('resize', this, function() {
919             self.$el.height(0);
920             lazyreflow();
921         });
922     },
923     start: function() {
924         this._super.apply(this, arguments);
925         this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container');
926         this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
927         return this.do_reload();
928     },
929     do_reload: function() {
930         var self = this;
931         return this.rpc("/web/menu/load", {}).done(function(r) {
932             self.menu_loaded(r);
933         });
934     },
935     menu_loaded: function(data) {
936         var self = this;
937         this.data = {data: data};
938         this.renderElement();
939         this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
940         this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
941         // Hide second level submenus
942         this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
943         if (self.current_menu) {
944             self.open_menu(self.current_menu);
945         }
946         this.trigger('menu_loaded', data);
947         this.has_been_loaded.resolve();
948     },
949     do_load_needaction: function (menu_ids) {
950         var self = this;
951         menu_ids = _.compact(menu_ids);
952         if (_.isEmpty(menu_ids)) {
953             return $.when();
954         }
955         return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
956             self.on_needaction_loaded(r);
957         });
958     },
959     on_needaction_loaded: function(data) {
960         var self = this;
961         this.needaction_data = data;
962         _.each(this.needaction_data, function (item, menu_id) {
963             var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
964             $item.find('.oe_menu_counter').remove();
965             if (item.needaction_counter && item.needaction_counter > 0) {
966                 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
967             }
968         });
969     },
970     /**
971      * Reflow the menu items and dock overflowing items into a "More" menu item.
972      * Automatically called when 'menu_loaded' event is triggered and on window resizing.
973      */
974     reflow: function() {
975         var self = this;
976         this.$el.height('auto').show();
977         var $more_container = this.$('.oe_menu_more_container').hide();
978         var $more = this.$('.oe_menu_more');
979         $more.children('li').insertBefore($more_container);
980         var $toplevel_items = this.$el.children('li').not($more_container).hide();
981         $toplevel_items.each(function() {
982             var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
983             self.$el.parent().children(':visible').each(function() {
984                 remaining_space -= $(this).outerWidth();
985             });
986             if ($(this).width() > remaining_space) {
987                 return false;
988             }
989             $(this).show();
990         });
991         $more.append($toplevel_items.filter(':hidden').show());
992         $more_container.toggle(!!$more.children().length);
993         // Hide toplevel item if there is only one
994         var $toplevel = this.$el.children("li:visible");
995         if ($toplevel.length === 1) {
996             $toplevel.hide();
997         }
998     },
999     /**
1000      * Opens a given menu by id, as if a user had browsed to that menu by hand
1001      * except does not trigger any event on the way
1002      *
1003      * @param {Number} id database id of the terminal menu to select
1004      */
1005     open_menu: function (id) {
1006         this.current_menu = id;
1007         this.session.active_id = id;
1008         var $clicked_menu, $sub_menu, $main_menu;
1009         $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
1010         this.trigger('open_menu', id, $clicked_menu);
1011
1012         if (this.$secondary_menus.has($clicked_menu).length) {
1013             $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
1014             $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
1015         } else {
1016             $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
1017             $main_menu = $clicked_menu;
1018         }
1019
1020         // Activate current main menu
1021         this.$el.find('.oe_active').removeClass('oe_active');
1022         $main_menu.addClass('oe_active');
1023
1024         // Show current sub menu
1025         this.$secondary_menus.find('.oe_secondary_menu').hide();
1026         $sub_menu.show();
1027
1028         // Hide/Show the leftbar menu depending of the presence of sub-items
1029         this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
1030
1031         // Activate current menu item and show parents
1032         this.$secondary_menus.find('.oe_active').removeClass('oe_active');
1033         if ($main_menu !== $clicked_menu) {
1034             $clicked_menu.parents().show();
1035             if ($clicked_menu.is('.oe_menu_toggler')) {
1036                 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
1037             } else {
1038                 $clicked_menu.parent().addClass('oe_active');
1039             }
1040         }
1041     },
1042     /**
1043      * Call open_menu with the first menu_item matching an action_id
1044      *
1045      * @param {Number} id the action_id to match
1046      */
1047     open_action: function (id) {
1048         var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
1049         var menu_id = $menu.data('menu');
1050         if (menu_id) {
1051             this.open_menu(menu_id);
1052         }
1053     },
1054     /**
1055      * Process a click on a menu item
1056      *
1057      * @param {Number} id the menu_id
1058      * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
1059      */
1060     menu_click: function(id, needaction) {
1061         if (!id) { return; }
1062
1063         // find back the menuitem in dom to get the action
1064         var $item = this.$el.find('a[data-menu=' + id + ']');
1065         if (!$item.length) {
1066             $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
1067         }
1068         var action_id = $item.data('action-id');
1069         // If first level menu doesnt have action trigger first leaf
1070         if (!action_id) {
1071             if(this.$el.has($item).length) {
1072                 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
1073                 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
1074                 if($items.length) {
1075                     action_id = $items.data('action-id');
1076                     id = $items.data('menu');
1077                 }
1078             }
1079         }
1080         if (action_id) {
1081             this.trigger('menu_click', {
1082                 action_id: action_id,
1083                 needaction: needaction,
1084                 id: id,
1085                 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
1086             }, $item);
1087         }
1088         this.open_menu(id);
1089     },
1090     do_reload_needaction: function () {
1091         var self = this;
1092         if (self.current_menu) {
1093             self.do_load_needaction([self.current_menu]).then(function () {
1094                 self.trigger("need_action_reloaded");
1095             });
1096         }
1097     },
1098     /**
1099      * Jquery event handler for menu click
1100      *
1101      * @param {Event} ev the jquery event
1102      */
1103     on_top_menu_click: function(ev) {
1104         var self = this;
1105         var id = $(ev.currentTarget).data('menu');
1106         var menu_ids = [id];
1107         var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
1108         function add_menu_ids (menu) {
1109             if (menu.children) {
1110                 _.each(menu.children, function (menu) {
1111                     menu_ids.push(menu.id);
1112                     add_menu_ids(menu);
1113                 });
1114             }
1115         }
1116         add_menu_ids(menu);
1117         self.do_load_needaction(menu_ids).then(function () {
1118             self.trigger("need_action_reloaded");
1119         });
1120         this.on_menu_click(ev);
1121     },
1122     on_menu_click: function(ev) {
1123         ev.preventDefault();
1124         var needaction = $(ev.target).is('div.oe_menu_counter');
1125         this.menu_click($(ev.currentTarget).data('menu'), needaction);
1126     },
1127 });
1128
1129 instance.web.UserMenu =  instance.web.Widget.extend({
1130     template: "UserMenu",
1131     init: function(parent) {
1132         this._super(parent);
1133         this.update_promise = $.Deferred().resolve();
1134     },
1135     start: function() {
1136         var self = this;
1137         this._super.apply(this, arguments);
1138         this.$el.on('click', '.oe_dropdown_menu li a[data-menu]', function(ev) {
1139             ev.preventDefault();
1140             var f = self['on_menu_' + $(this).data('menu')];
1141             if (f) {
1142                 f($(this));
1143             }
1144         });
1145     },
1146     do_update: function () {
1147         var self = this;
1148         var fct = function() {
1149             var $avatar = self.$el.find('.oe_topbar_avatar');
1150             $avatar.attr('src', $avatar.data('default-src'));
1151             if (!self.session.uid)
1152                 return;
1153             var func = new instance.web.Model("res.users").get_func("read");
1154             return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1155                 var topbar_name = res.name;
1156                 if(instance.session.debug)
1157                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1158                 if(res.company_id[0] > 1)
1159                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1160                 self.$el.find('.oe_topbar_name').text(topbar_name);
1161                 if (!instance.session.debug) {
1162                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1163                 }
1164                 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1165                 $avatar.attr('src', avatar_src);
1166             });
1167         };
1168         this.update_promise = this.update_promise.then(fct, fct);
1169     },
1170     on_menu_help: function() {
1171         window.open('http://help.openerp.com', '_blank');
1172     },
1173     on_menu_logout: function() {
1174         this.trigger('user_logout');
1175     },
1176     on_menu_settings: function() {
1177         var self = this;
1178         if (!this.getParent().has_uncommitted_changes()) {
1179             self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1180                 result.res_id = instance.session.uid;
1181                 self.getParent().action_manager.do_action(result);
1182             });
1183         }
1184     },
1185     on_menu_account: function() {
1186         var self = this;
1187         if (!this.getParent().has_uncommitted_changes()) {
1188             var P = new instance.web.Model('ir.config_parameter');
1189             P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1190                 var state = {
1191                             'd': instance.session.db,
1192                             'u': window.location.protocol + '//' + window.location.host,
1193                         };
1194                 var params = {
1195                     response_type: 'token',
1196                     client_id: dbuuid || '',
1197                     state: JSON.stringify(state),
1198                     scope: 'userinfo',
1199                 };
1200                 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1201             });
1202         }
1203     },
1204     on_menu_about: function() {
1205         var self = this;
1206         self.rpc("/web/webclient/version_info", {}).done(function(res) {
1207             var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1208             $help.find('a.oe_activate_debug_mode').click(function (e) {
1209                 e.preventDefault();
1210                 window.location = $.param.querystring( window.location.href, 'debug');
1211             });
1212             instance.web.dialog($help, {autoOpen: true,
1213                 modal: true, width: 507, height: 290, resizable: false, title: _t("About")});
1214         });
1215     },
1216 });
1217
1218 instance.web.Client = instance.web.Widget.extend({
1219     init: function(parent, origin) {
1220         instance.client = instance.webclient = this;
1221         this._super(parent);
1222         this.origin = origin;
1223     },
1224     start: function() {
1225         var self = this;
1226         return instance.session.session_bind(this.origin).then(function() {
1227             var $e = $(QWeb.render(self._template, {widget: self}));
1228             self.replaceElement($e);
1229             $e.openerpClass();
1230             self.bind_events();
1231             return self.show_common();
1232         });
1233     },
1234     bind_events: function() {
1235         var self = this;
1236         this.$el.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() {
1237             $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter');
1238         });
1239         this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1240             ev.preventDefault();
1241             var $toggle = $(this);
1242             var $menu = $toggle.siblings('.oe_dropdown_menu');
1243             $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1244             var state = $menu.is('.oe_opened');
1245             setTimeout(function() {
1246                 // Do not alter propagation
1247                 $toggle.add($menu).toggleClass('oe_opened', !state);
1248                 if (!state) {
1249                     // Move $menu if outside window's edge
1250                     var doc_width = $(document).width();
1251                     var offset = $menu.offset();
1252                     var menu_width = $menu.width();
1253                     var x = doc_width - offset.left - menu_width - 2;
1254                     if (x < 0) {
1255                         $menu.offset({ left: offset.left + x }).width(menu_width);
1256                     }
1257                 }
1258             }, 0);
1259         });
1260         instance.web.bus.on('click', this, function(ev) {
1261             $.fn.tipsy.clear();
1262             if (!$(ev.target).is('input[type=file]')) {
1263                 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1264             }
1265         });
1266     },
1267     show_common: function() {
1268         var self = this;
1269         this.crashmanager =  new instance.web.CrashManager();
1270         instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1271         self.notification = new instance.web.Notification(this);
1272         self.notification.appendTo(self.$el);
1273         self.loading = new instance.web.Loading(self);
1274         self.loading.appendTo(self.$el);
1275         self.action_manager = new instance.web.ActionManager(self);
1276         self.action_manager.appendTo(self.$('.oe_application'));
1277     },
1278     toggle_bars: function(value) {
1279         this.$('tr:has(td.oe_topbar),.oe_leftbar').toggle(value);
1280     },
1281     has_uncommitted_changes: function() {
1282         return false;
1283     },
1284 });
1285
1286 instance.web.WebClient = instance.web.Client.extend({
1287     _template: 'WebClient',
1288     events: {
1289         'click .oe_logo_edit_admin': 'logo_edit'
1290     },
1291     init: function(parent) {
1292         this._super(parent);
1293         this._current_state = null;
1294         this.menu_dm = new instance.web.DropMisordered();
1295         this.action_mutex = new $.Mutex();
1296         this.set('title_part', {"zopenerp": "OpenERP"});
1297     },
1298     start: function() {
1299         var self = this;
1300         this.on("change:title_part", this, this._title_changed);
1301         this._title_changed();
1302         return $.when(this._super()).then(function() {
1303             if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1304                 $("body").addClass("kitten-mode-activated");
1305                 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1306                 if ($.blockUI) {
1307                     var imgkit = Math.floor(Math.random() * 2 + 1);
1308                     $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1309                 }
1310             }
1311             if (!self.session.session_is_valid()) {
1312                 self.show_login();
1313             } else {
1314                 self.show_application();
1315             }
1316         });
1317     },
1318     /**
1319         Sets the first part of the title of the window, dedicated to the current action.
1320     */
1321     set_title: function(title) {
1322         this.set_title_part("action", title);
1323     },
1324     /**
1325         Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1326         a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1327         title of the window separated by '-'.
1328     */
1329     set_title_part: function(part, title) {
1330         var tmp = _.clone(this.get("title_part"));
1331         tmp[part] = title;
1332         this.set("title_part", tmp);
1333     },
1334     _title_changed: function() {
1335         var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1336         var tmp = "";
1337         _.each(parts, function(part) {
1338             var str = this.get("title_part")[part];
1339             if (str) {
1340                 tmp = tmp ? tmp + " - " + str : str;
1341             }
1342         }, this);
1343         document.title = tmp;
1344     },
1345     show_common: function() {
1346         var self = this;
1347         this._super();
1348         window.onerror = function (message, file, line) {
1349             self.crashmanager.show_error({
1350                 type: _t("Client Error"),
1351                 message: message,
1352                 data: {debug: file + ':' + line}
1353             });
1354         };
1355     },
1356     show_login: function() {
1357         this.toggle_bars(false);
1358
1359         var state = $.bbq.getState(true);
1360         var action = {
1361             type: 'ir.actions.client',
1362             tag: 'login',
1363             _push_me: false,
1364         };
1365
1366         this.action_manager.do_action(action);
1367         this.action_manager.inner_widget.on('login_successful', this, function() {
1368             this.show_application();        // will load the state we just pushed
1369         });
1370     },
1371     show_application: function() {
1372         var self = this;
1373         self.toggle_bars(true);
1374         self.update_logo();
1375         self.menu = new instance.web.Menu(self);
1376         self.menu.replace(this.$el.find('.oe_menu_placeholder'));
1377         self.menu.on('menu_click', this, this.on_menu_action);
1378         self.user_menu = new instance.web.UserMenu(self);
1379         self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder'));
1380         self.user_menu.on('user_logout', self, self.on_logout);
1381         self.user_menu.do_update();
1382         self.bind_hashchange();
1383         self.set_title();
1384         self.check_timezone();
1385     },
1386     update_logo: function() {
1387         var img = this.session.url('/web/binary/company_logo');
1388         this.$('.oe_logo img').attr('src', '').attr('src', img);
1389         this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1390     },
1391     logo_edit: function(ev) {
1392         var self = this;
1393         self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1394             self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1395                 result.res_id = res['company_id'][0];
1396                 result.target = "new";
1397                 result.views = [[false, 'form']];
1398                 result.flags = {
1399                     action_buttons: true,
1400                 };
1401                 self.action_manager.do_action(result);
1402                 var form = self.action_manager.dialog_widget.views.form.controller;
1403                 form.on("on_button_cancel", self.action_manager.dialog, self.action_manager.dialog.close);
1404                 form.on('record_saved', self, function() {
1405                     self.action_manager.dialog.close();
1406                     self.update_logo();
1407                 });
1408             });
1409         });
1410         return false;
1411     },
1412     check_timezone: function() {
1413         var self = this;
1414         return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1415             var user_offset = result[0]['tz_offset'];
1416             var offset = -(new Date().getTimezoneOffset());
1417             // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1418             var browser_offset = (offset < 0) ? "-" : "+";
1419             browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1420             browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1421             if (browser_offset !== user_offset) {
1422                 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1423                 $icon.on('click', function() {
1424                     var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1425                         user_timezone: instance.session.user_context.tz || 'UTC',
1426                         user_offset: user_offset,
1427                         browser_offset: browser_offset,
1428                     }), true);
1429                     notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1430                         notification.close();
1431                     }).find('a').on('click', function() {
1432                         notification.close();
1433                         self.user_menu.on_menu_settings();
1434                         return false;
1435                     });
1436                 });
1437                 $icon.appendTo(self.$('.oe_systray'));
1438             }
1439         });
1440     },
1441     destroy_content: function() {
1442         _.each(_.clone(this.getChildren()), function(el) {
1443             el.destroy();
1444         });
1445         this.$el.children().remove();
1446     },
1447     do_reload: function() {
1448         var self = this;
1449         return this.session.session_reload().then(function () {
1450             instance.session.load_modules(true).then(
1451                 self.menu.proxy('do_reload')); });
1452
1453     },
1454     do_notify: function() {
1455         var n = this.notification;
1456         return n.notify.apply(n, arguments);
1457     },
1458     do_warn: function() {
1459         var n = this.notification;
1460         return n.warn.apply(n, arguments);
1461     },
1462     on_logout: function() {
1463         var self = this;
1464         if (!this.has_uncommitted_changes()) {
1465             this.session.session_logout().done(function () {
1466                 $(window).unbind('hashchange', self.on_hashchange);
1467                 self.do_push_state({});
1468                 window.location.reload();
1469             });
1470         }
1471     },
1472     bind_hashchange: function() {
1473         var self = this;
1474         $(window).bind('hashchange', this.on_hashchange);
1475
1476         var state = $.bbq.getState(true);
1477         if (_.isEmpty(state) || state.action == "login") {
1478             self.menu.has_been_loaded.done(function() {
1479                 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1480                 if(first_menu_id) {
1481                     self.menu.menu_click(first_menu_id);
1482                 }
1483             });
1484         } else {
1485             $(window).trigger('hashchange');
1486         }
1487     },
1488     on_hashchange: function(event) {
1489         var self = this;
1490         var stringstate = event.getState(false);
1491         if (!_.isEqual(this._current_state, stringstate)) {
1492             var state = event.getState(true);
1493             if(!state.action && state.menu_id) {
1494                 self.menu.has_been_loaded.done(function() {
1495                     self.menu.do_reload().done(function() {
1496                         self.menu.menu_click(state.menu_id);
1497                     });
1498                 });
1499             } else {
1500                 state._push_me = false;  // no need to push state back...
1501                 this.action_manager.do_load_state(state, !!this._current_state);
1502             }
1503         }
1504         this._current_state = stringstate;
1505     },
1506     do_push_state: function(state) {
1507         this.set_title(state.title);
1508         delete state.title;
1509         var url = '#' + $.param(state);
1510         this._current_state = $.deparam($.param(state), false);     // stringify all values
1511         $.bbq.pushState(url);
1512         this.trigger('state_pushed', state);
1513     },
1514     on_menu_action: function(options) {
1515         var self = this;
1516         return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1517             .then(function (result) {
1518                 return self.action_mutex.exec(function() {
1519                     if (options.needaction) {
1520                         result.context = new instance.web.CompoundContext(result.context, {
1521                             search_default_message_unread: true,
1522                             search_disable_custom_filters: true,
1523                         });
1524                     }
1525                     var completed = $.Deferred();
1526                     $.when(self.action_manager.do_action(result, {
1527                         clear_breadcrumbs: true,
1528                         action_menu_id: self.menu.current_menu,
1529                     })).fail(function() {
1530                         self.menu.open_menu(options.previous_menu_id);
1531                     }).always(function() {
1532                         completed.resolve();
1533                     });
1534                     setTimeout(function() {
1535                         completed.resolve();
1536                     }, 2000);
1537                     // We block the menu when clicking on an element until the action has correctly finished
1538                     // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1539                     return completed;
1540                 });
1541             });
1542     },
1543     set_content_full_screen: function(fullscreen) {
1544         $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1545         this.$('.oe_webclient').toggleClass(
1546             'oe_content_full_screen', fullscreen);
1547     },
1548     has_uncommitted_changes: function() {
1549         var $e = $.Event('clear_uncommitted_changes');
1550         instance.web.bus.trigger('clear_uncommitted_changes', $e);
1551         if ($e.isDefaultPrevented()) {
1552             return true;
1553         } else {
1554             return this._super.apply(this, arguments);
1555         }
1556     },
1557 });
1558
1559 instance.web.EmbeddedClient = instance.web.Client.extend({
1560     _template: 'EmbedClient',
1561     init: function(parent, origin, dbname, login, key, action_id, options) {
1562         this._super(parent, origin);
1563         this.bind_credentials(dbname, login, key);
1564         this.action_id = action_id;
1565         this.options = options || {};
1566     },
1567     start: function() {
1568         var self = this;
1569         return $.when(this._super()).then(function() {
1570             return self.authenticate().then(function() {
1571                 if (!self.action_id) {
1572                     return;
1573                 }
1574                 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1575                     var action = result;
1576                     action.flags = _.extend({
1577                         //views_switcher : false,
1578                         search_view : false,
1579                         action_buttons : false,
1580                         sidebar : false
1581                         //pager : false
1582                     }, self.options, action.flags || {});
1583
1584                     self.do_action(action);
1585                 });
1586             });
1587         });
1588     },
1589
1590     do_action: function(/*...*/) {
1591         var am = this.action_manager;
1592         return am.do_action.apply(am, arguments);
1593     },
1594
1595     authenticate: function() {
1596         var s = instance.session;
1597         if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1598             return $.when();
1599         }
1600         return instance.session.session_authenticate(this.dbname, this.login, this.key);
1601     },
1602
1603     bind_credentials: function(dbname, login, key) {
1604         this.dbname = dbname;
1605         this.login = login;
1606         this.key = key;
1607     },
1608
1609 });
1610
1611 instance.web.embed = function (origin, dbname, login, key, action, options) {
1612     $('head').append($('<link>', {
1613         'rel': 'stylesheet',
1614         'type': 'text/css',
1615         'href': origin +'/web/webclient/css'
1616     }));
1617     var currentScript = document.currentScript;
1618     if (!currentScript) {
1619         var sc = document.getElementsByTagName('script');
1620         currentScript = sc[sc.length-1];
1621     }
1622     var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1623     client.insertAfter(currentScript);
1624 };
1625
1626 })();
1627
1628 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: