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