[MERGE] 6.1 changes and trunk
[odoo/odoo.git] / addons / web / static / src / js / views.js
1 /*---------------------------------------------------------
2  * OpenERP web library
3  *---------------------------------------------------------*/
4
5 openerp.web.views = function(instance) {
6 var QWeb = instance.web.qweb,
7     _t = instance.web._t;
8
9 instance.web.ActionManager = instance.web.Widget.extend({
10     init: function(parent) {
11         this._super(parent);
12         this.inner_action = null;
13         this.inner_viewmanager = null;
14         this.dialog = null;
15         this.dialog_viewmanager = null;
16         this.client_widget = null;
17     },
18     dialog_stop: function () {
19         if (this.dialog) {
20             this.dialog_viewmanager.destroy();
21             this.dialog_viewmanager = null;
22             this.dialog.destroy();
23             this.dialog = null;
24         }
25     },
26     content_stop: function () {
27         if (this.inner_viewmanager) {
28             this.inner_viewmanager.destroy();
29             this.inner_viewmanager = null;
30         }
31         if (this.client_widget) {
32             this.client_widget.destroy();
33             this.client_widget = null;
34         }
35     },
36     do_push_state: function(state) {
37         if (this.getParent() && this.getParent().do_push_state) {
38             if (this.inner_action) {
39                 state['title'] = this.inner_action.name;
40                 state['model'] = this.inner_action.res_model;
41                 if (this.inner_action.id) {
42                     state['action_id'] = this.inner_action.id;
43                 }
44             }
45             this.getParent().do_push_state(state);
46         }
47     },
48     do_load_state: function(state, warm) {
49         var self = this,
50             action_loaded;
51         if (state.action_id) {
52             var run_action = (!this.inner_viewmanager) || this.inner_viewmanager.action.id !== state.action_id;
53             if (run_action) {
54                 this.null_action();
55                 action_loaded = this.do_action(state.action_id);
56                 instance.webclient.menu.has_been_loaded.then(function() {
57                     instance.webclient.menu.open_action(state.action_id);
58                 });
59             }
60         } else if (state.model && state.id) {
61             // TODO handle context & domain ?
62             this.null_action();
63             var action = {
64                 res_model: state.model,
65                 res_id: state.id,
66                 type: 'ir.actions.act_window',
67                 views: [[false, 'form']]
68             };
69             action_loaded = this.do_action(action);
70         } else if (state.sa) {
71             // load session action
72             var self = this;
73             this.null_action();
74             action_loaded = this.rpc('/web/session/get_session_action',  {key: state.sa}).pipe(function(action) {
75                 if (action) {
76                     return self.do_action(action);
77                 }
78             });
79         } else if (state.client_action) {
80             this.null_action();
81             var action = state.client_action;
82             if(_.isString(action)) {
83                 action = {
84                     tag: action,
85                     params: state,
86                 };
87             }
88             this.ir_actions_client(action);
89         }
90
91         $.when(action_loaded || null).then(function() {
92             if (self.inner_viewmanager) {
93                 self.inner_viewmanager.do_load_state(state, warm);
94             }
95         });
96     },
97     do_action: function(action, on_close) {
98         if (_.isNumber(action)) {
99             var self = this;
100             return self.rpc("/web/action/load", { action_id: action }, function(result) {
101                 self.do_action(result.result, on_close);
102             });
103         }
104         if (!action.type) {
105             console.error("No type for action", action);
106             return;
107         }
108         var type = action.type.replace(/\./g,'_');
109         var popup = action.target === 'new';
110         var inline = action.target === 'inline';
111         action.flags = _.extend({
112             views_switcher : !popup && !inline,
113             search_view : !popup && !inline,
114             action_buttons : !popup && !inline,
115             sidebar : !popup && !inline,
116             pager : !popup && !inline,
117             display_title : !popup
118         }, action.flags || {});
119         if (!(type in this)) {
120             console.error("Action manager can't handle action of type " + action.type, action);
121             return;
122         }
123         return this[type](action, on_close);
124     },
125     null_action: function() {
126         this.dialog_stop();
127         this.content_stop();
128     },
129     ir_actions_act_window: function (action, on_close) {
130         var self = this;
131         if (_(['base.module.upgrade', 'base.setup.installer'])
132                 .contains(action.res_model)) {
133             var old_close = on_close;
134             on_close = function () {
135                 instance.webclient.do_reload().then(old_close);
136             };
137         }
138         if (action.target === 'new') {
139             if (this.dialog == null) {
140                 this.dialog = new instance.web.Dialog(this, { width: '80%' });
141                 if(on_close)
142                     this.dialog.on_close.add(on_close);
143             } else {
144                 this.dialog_viewmanager.destroy();
145             }
146             this.dialog.dialog_title = action.name;
147             this.dialog_viewmanager = new instance.web.ViewManagerAction(this, action);
148             this.dialog_viewmanager.appendTo(this.dialog.$element);
149             this.dialog.open();
150         } else  {
151             this.dialog_stop();
152             this.content_stop();
153             if(action.menu_id) {
154                 return this.getParent().do_action(action, function () {
155                     instance.webclient.menu.open_menu(action.menu_id);
156                 });
157             }
158             this.inner_action = action;
159             this.inner_viewmanager = new instance.web.ViewManagerAction(this, action);
160             this.inner_viewmanager.appendTo(this.$element);
161             this.inner_viewmanager.$element.addClass("oe_view_manager_global");
162         }
163     },
164     ir_actions_act_window_close: function (action, on_closed) {
165         if (!this.dialog && on_closed) {
166             on_closed();
167         }
168         this.dialog_stop();
169     },
170     ir_actions_server: function (action, on_closed) {
171         var self = this;
172         this.rpc('/web/action/run', {
173             action_id: action.id,
174             context: action.context || {}
175         }).then(function (action) {
176             self.do_action(action, on_closed)
177         });
178     },
179     ir_actions_client: function (action) {
180         this.content_stop();
181         this.dialog_stop();
182         var ClientWidget = instance.web.client_actions.get_object(action.tag);
183         (this.client_widget = new ClientWidget(this, action.params)).appendTo(this.$element);
184     },
185     ir_actions_report_xml: function(action, on_closed) {
186         var self = this;
187         $.blockUI();
188         self.rpc("/web/session/eval_domain_and_context", {
189             contexts: [action.context],
190             domains: []
191         }).then(function(res) {
192             action = _.clone(action);
193             action.context = res.context;
194             self.session.get_file({
195                 url: '/web/report',
196                 data: {action: JSON.stringify(action)},
197                 complete: $.unblockUI,
198                 success: function(){
199                     if (!self.dialog && on_closed) {
200                         on_closed();
201                     }
202                     self.dialog_stop();
203                 },
204                 error: instance.webclient.crashmanager.on_rpc_error
205             })
206         });
207     },
208     ir_actions_act_url: function (action) {
209         window.open(action.url, action.target === 'self' ? '_self' : '_blank');
210     },
211     ir_ui_menu: function (action) {
212         this.getParent().do_action(action);
213     }
214 });
215
216 instance.web.ViewManager =  instance.web.Widget.extend({
217     template: "ViewManager",
218     init: function(parent, dataset, views, flags) {
219         this._super(parent);
220         this.model = dataset ? dataset.model : undefined;
221         this.dataset = dataset;
222         this.searchview = null;
223         this.active_view = null;
224         this.views_src = _.map(views, function(x) {
225             if (x instanceof Array) {
226                 var View = instance.web.views.get_object(x[1], true);
227                 return {
228                     view_id: x[0],
229                     view_type: x[1],
230                     label: View ? View.prototype.display_name : (void 'nope')
231                 };
232             } else {
233                 return x;
234             }
235         });
236         this.views = {};
237         this.flags = flags || {};
238         this.registry = instance.web.views;
239         this.views_history = [];
240     },
241     /**
242      * @returns {jQuery.Deferred} initial view loading promise
243      */
244     start: function() {
245         this._super();
246         var self = this;
247         this.$element.find('.oe_view_manager_switch a').click(function() {
248             self.on_mode_switch($(this).data('view-type'));
249         }).tipsy();
250         var views_ids = {};
251         _.each(this.views_src, function(view) {
252             self.views[view.view_type] = $.extend({}, view, {
253                 deferred : $.Deferred(),
254                 controller : null,
255                 options : _.extend({
256                     $buttons : self.$element.find('.oe_view_manager_buttons'),
257                     $sidebar : self.flags.sidebar ? self.$element.find('.oe_view_manager_sidebar') : undefined,
258                     $pager : self.$element.find('.oe_view_manager_pager'),
259                     action : self.action,
260                     action_views_ids : views_ids
261                 }, self.flags, self.flags[view.view_type] || {}, view.options || {})
262             });
263             views_ids[view.view_type] = view.view_id;
264         });
265         if (this.flags.views_switcher === false) {
266             this.$element.find('.oe_view_manager_switch').hide();
267         }
268         // If no default view defined, switch to the first one in sequence
269         var default_view = this.flags.default_view || this.views_src[0].view_type;
270         return this.on_mode_switch(default_view);
271     },
272     /**
273      * Asks the view manager to switch visualization mode.
274      *
275      * @param {String} view_type type of view to display
276      * @param {Boolean} [no_store=false] don't store the view being switched to on the switch stack
277      * @returns {jQuery.Deferred} new view loading promise
278      */
279     on_mode_switch: function(view_type, no_store, view_options) {
280         var self = this;
281         var view = this.views[view_type];
282         var view_promise;
283         if(!view)
284             return $.Deferred().reject();
285
286         if (!no_store) {
287             this.views_history.push(view_type);
288         }
289         this.active_view = view_type;
290
291         if (!view.controller) {
292             // Lazy loading of views
293             var controllerclass = this.registry.get_object(view_type);
294             var options = _.clone(view.options);
295             if (view_type === "form" && this.action) {
296                 switch (this.action.target) {
297                     case 'new':
298                     case 'inline':
299                         options.initial_mode = 'edit';
300                         break;
301                 }
302             }
303             var controller = new controllerclass(this, this.dataset, view.view_id, options);
304             if (view.embedded_view) {
305                 controller.set_embedded_view(view.embedded_view);
306             }
307             controller.do_switch_view.add_last(_.bind(this.switch_view, this));
308             controller.do_prev_view.add_last(this.on_prev_view);
309             var container = this.$element.find(".oe_view_manager_view_" + view_type);
310             view_promise = controller.appendTo(container);
311             this.views[view_type].controller = controller;
312             this.views[view_type].deferred.resolve(view_type);
313             $.when(view_promise).then(function() {
314                 self.on_controller_inited(view_type, controller);
315                 if (self.searchview
316                         && self.flags.auto_search
317                         && view.controller.searchable !== false) {
318                     self.searchview.ready.then(self.searchview.do_search);
319                 }
320             });
321         } else if (this.searchview
322                 && self.flags.auto_search
323                 && view.controller.searchable !== false) {
324             this.searchview.ready.then(this.searchview.do_search);
325         }
326
327         if (this.searchview) {
328             this.searchview[(view.controller.searchable === false || this.searchview.hidden) ? 'hide' : 'show']();
329         }
330
331         this.$element
332             .find('.oe_view_manager_switch a').parent().removeClass('active');
333         this.$element
334             .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
335             .parent().addClass('active');
336
337         $.when(view_promise).then(function () {
338             _.each(_.keys(self.views), function(view_name) {
339                 var controller = self.views[view_name].controller;
340                 if (controller) {
341                     var container = self.$element.find(".oe_view_manager_view_" + view_name + ":first");
342                     if (view_name === view_type) {
343                         container.show();
344                         controller.do_show(view_options || {});
345                     } else {
346                         container.hide();
347                         controller.do_hide();
348                     }
349                 }
350             });
351
352             self.$element.find('.oe_view_title_text:first').text(
353                     self.display_title());
354         });
355         return view_promise;
356     },
357     /**
358      * Method used internally when a view asks to switch view. This method is meant
359      * to be extended by child classes to change the default behavior, which simply
360      * consist to switch to the asked view.
361      */
362     switch_view: function(view_type, no_store, options) {
363         return this.on_mode_switch(view_type, no_store, options);
364     },
365     /**
366      * Returns to the view preceding the caller view in this manager's
367      * navigation history (the navigation history is appended to via
368      * on_mode_switch)
369      *
370      * @param {Object} [options]
371      * @param {Boolean} [options.created=false] resource was created
372      * @param {String} [options.default=null] view to switch to if no previous view
373      * @returns {$.Deferred} switching end signal
374      */
375     on_prev_view: function (options) {
376         options = options || {};
377         var current_view = this.views_history.pop();
378         var previous_view = this.views_history[this.views_history.length - 1] || options['default'];
379         if (options.created && current_view === 'form' && previous_view === 'list') {
380             // APR special case: "If creation mode from list (and only from a list),
381             // after saving, go to page view (don't come back in list)"
382             return this.on_mode_switch('form');
383         } else if (options.created && !previous_view && this.action && this.action.flags.default_view === 'form') {
384             // APR special case: "If creation from dashboard, we have no previous view
385             return this.on_mode_switch('form');
386         }
387         return this.on_mode_switch(previous_view, true);
388     },
389     /**
390      * Sets up the current viewmanager's search view.
391      *
392      * @param {Number|false} view_id the view to use or false for a default one
393      * @returns {jQuery.Deferred} search view startup deferred
394      */
395     setup_search_view: function(view_id, search_defaults) {
396         var self = this;
397         if (this.searchview) {
398             this.searchview.destroy();
399         }
400         this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, this.flags.search_view === false);
401
402         this.searchview.on_search.add(this.do_searchview_search);
403         return this.searchview.appendTo(this.$element.find(".oe_view_manager_view_search"));
404     },
405     do_searchview_search: function(domains, contexts, groupbys) {
406         var self = this,
407             controller = this.views[this.active_view].controller,
408             action_context = this.action.context || {};
409         this.rpc('/web/session/eval_domain_and_context', {
410             domains: [this.action.domain || []].concat(domains || []),
411             contexts: [action_context].concat(contexts || []),
412             group_by_seq: groupbys || []
413         }, function (results) {
414             self.dataset._model = new instance.web.Model(
415                 self.dataset.model, results.context, results.domain);
416             var groupby = results.group_by.length
417                         ? results.group_by
418                         : action_context.group_by;
419             if (_.isString(groupby)) {
420                 groupby = [groupby];
421             }
422             controller.do_search(results.domain, results.context, groupby || []);
423         });
424     },
425     /**
426      * Event launched when a controller has been inited.
427      *
428      * @param {String} view_type type of view
429      * @param {String} view the inited controller
430      */
431     on_controller_inited: function(view_type, view) {
432     },
433     /**
434      * Called when one of the view want to execute an action
435      */
436     on_action: function(action) {
437     },
438     on_create: function() {
439     },
440     on_remove: function() {
441     },
442     on_edit: function() {
443     },
444     /**
445      * Called by children view after executing an action
446      */
447     on_action_executed: function () {
448     },
449     display_title: function () {
450         var view = this.views[this.active_view];
451         if (view) {
452             // ick
453             return view.controller.fields_view.arch.attrs.string;
454         }
455         return '';
456     }
457 });
458
459 instance.web.ViewManagerAction = instance.web.ViewManager.extend({
460     template:"ViewManagerAction",
461     /**
462      * @constructs instance.web.ViewManagerAction
463      * @extends instance.web.ViewManager
464      *
465      * @param {instance.web.ActionManager} parent parent object/widget
466      * @param {Object} action descriptor for the action this viewmanager needs to manage its views.
467      */
468     init: function(parent, action) {
469         // dataset initialization will take the session from ``this``, so if we
470         // do not have it yet (and we don't, because we've not called our own
471         // ``_super()``) rpc requests will blow up.
472         var flags = action.flags || {};
473         if (!('auto_search' in flags)) {
474             flags.auto_search = action.auto_search !== false;
475         }
476         if (action.res_model == 'board.board' && action.view_mode === 'form') {
477             // Special case for Dashboards
478             _.extend(flags, {
479                 views_switcher : false,
480                 display_title : false,
481                 search_view : false,
482                 pager : false,
483                 sidebar : false,
484                 action_buttons : false
485             });
486         }
487         this._super(parent, null, action.views, flags);
488         this.session = parent.session;
489         this.action = action;
490         var dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain);
491         if (action.res_id) {
492             dataset.ids.push(action.res_id);
493             dataset.index = 0;
494         }
495         this.dataset = dataset;
496
497         // setup storage for session-wise menu hiding
498         if (this.session.hidden_menutips) {
499             return;
500         }
501         this.session.hidden_menutips = {}
502     },
503     /**
504      * Initializes the ViewManagerAction: sets up the searchview (if the
505      * searchview is enabled in the manager's action flags), calls into the
506      * parent to initialize the primary view and (if the VMA has a searchview)
507      * launches an initial search after both views are done rendering.
508      */
509     start: function() {
510         var self = this,
511             searchview_loaded,
512             search_defaults = {};
513         _.each(this.action.context, function (value, key) {
514             var match = /^search_default_(.*)$/.exec(key);
515             if (match) {
516                 search_defaults[match[1]] = value;
517             }
518         });
519         // init search view
520         var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
521
522         searchview_loaded = this.setup_search_view(searchview_id || false, search_defaults);
523
524         var main_view_loaded = this._super();
525
526         var manager_ready = $.when(searchview_loaded, main_view_loaded);
527
528         this.$element.find('.oe_debug_view').change(this.on_debug_changed);
529
530         if (this.action.help && !this.flags.low_profile) {
531             var Users = new instance.web.DataSet(self, 'res.users'),
532                 $tips = this.$element.find('.oe_view_manager_menu_tips');
533             $tips.delegate('blockquote button', 'click', function() {
534                 var $this = $(this);
535                 //noinspection FallthroughInSwitchStatementJS
536                 switch ($this.attr('name')) {
537                 case 'disable':
538                     Users.write(self.session.uid, {menu_tips:false});
539                 case 'hide':
540                     $this.closest('blockquote').hide();
541                     self.session.hidden_menutips[self.action.id] = true;
542                 }
543             });
544             if (!(self.action.id in self.session.hidden_menutips)) {
545                 Users.read_ids([this.session.uid], ['menu_tips']).then(function(users) {
546                     var user = users[0];
547                     if (!(user && user.id === self.session.uid)) {
548                         return;
549                     }
550                     $tips.find('blockquote').toggle(user.menu_tips);
551                 });
552             }
553         }
554
555         return manager_ready;
556     },
557     on_debug_changed: function (evt) {
558         var self = this,
559             $sel = $(evt.currentTarget),
560             $option = $sel.find('option:selected'),
561             val = $sel.val(),
562             current_view = this.views[this.active_view].controller;
563         switch (val) {
564             case 'fvg':
565                 var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get"), width: '95%' }).open();
566                 $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$element);
567                 break;
568             case 'perm_read':
569                 var ids = current_view.get_selected_ids();
570                 if (ids.length === 1) {
571                     this.dataset.call('perm_read', [ids]).then(function(result) {
572                         var dialog = new instance.web.Dialog(this, {
573                             title: _.str.sprintf(_t("View Log (%s)"), self.dataset.model),
574                             width: 400
575                         }, QWeb.render('ViewManagerDebugViewLog', {
576                             perm : result[0],
577                             format : instance.web.format_value
578                         })).open();
579                     });
580                 }
581                 break;
582             case 'toggle_layout_outline':
583                 current_view.rendering_engine.toggle_layout_debugging();
584                 break;
585             case 'fields':
586                 this.dataset.call_and_eval(
587                         'fields_get', [false, {}], null, 1).then(function (fields) {
588                     var $root = $('<dl>');
589                     _(fields).each(function (attributes, name) {
590                         $root.append($('<dt>').append($('<h4>').text(name)));
591                         var $attrs = $('<dl>').appendTo(
592                                 $('<dd>').appendTo($root));
593                         _(attributes).each(function (def, name) {
594                             if (def instanceof Object) {
595                                 def = JSON.stringify(def);
596                             }
597                             $attrs
598                                 .append($('<dt>').text(name))
599                                 .append($('<dd style="white-space: pre-wrap;">').text(def));
600                         });
601                     });
602                     new instance.web.Dialog(self, {
603                         title: _.str.sprintf(_t("Model %s fields"),
604                                              self.dataset.model),
605                         width: '95%'}, $root).open();
606                 });
607                 break;
608             case 'manage_views':
609                 if (current_view.fields_view && current_view.fields_view.arch) {
610                     var view_editor = new instance.web.ViewEditor(current_view, current_view.$element, this.dataset, current_view.fields_view.arch);
611                     view_editor.start();
612                 } else {
613                     this.do_warn(_t("Manage Views"),
614                             _t("Could not find current view declaration"));
615                 }
616                 break;
617             case 'edit_workflow':
618                 return this.do_action({
619                     res_model : 'workflow',
620                     domain : [['osv', '=', this.dataset.model]],
621                     views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
622                     type : 'ir.actions.act_window',
623                     view_type : 'list',
624                     view_mode : 'list'
625                 });
626                 break;
627             case 'edit':
628                 this.do_edit_resource($option.data('model'), $option.data('id'), { name : $option.text() });
629                 break;
630             case 'manage_filters':
631                 this.do_action({
632                     res_model: 'ir.filters',
633                     views: [[false, 'list'], [false, 'form']],
634                     type: 'ir.actions.act_window',
635                     context: {
636                         search_default_my_filters: true,
637                         search_default_model_id: this.dataset.model
638                     }
639                 });
640                 break;
641             default:
642                 if (val) {
643                     console.log("No debug handler for ", val);
644                 }
645         }
646         evt.currentTarget.selectedIndex = 0;
647     },
648     do_edit_resource: function(model, id, action) {
649         var action = _.extend({
650             res_model : model,
651             res_id : id,
652             type : 'ir.actions.act_window',
653             view_type : 'form',
654             view_mode : 'form',
655             views : [[false, 'form']],
656             target : 'new',
657             flags : {
658                 action_buttons : true,
659                 form : {
660                     resize_textareas : true
661                 }
662             }
663         }, action || {});
664         this.do_action(action);
665     },
666     on_mode_switch: function (view_type, no_store, options) {
667         var self = this;
668
669         return $.when(this._super.apply(this, arguments)).then(function () {
670             var controller = self.views[self.active_view].controller,
671                 fvg = controller.fields_view,
672                 view_id = (fvg && fvg.view_id) || '--';
673             self.$element.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
674                 view: controller,
675                 view_manager: self
676             }));
677             if (!self.action.name && fvg) {
678                 self.$element.find('.oe_view_title_text').text(fvg.arch.attrs.string || fvg.name);
679             }
680
681         });
682     },
683     do_push_state: function(state) {
684         if (this.getParent() && this.getParent().do_push_state) {
685             state["view_type"] = this.active_view;
686             this.getParent().do_push_state(state);
687         }
688     },
689     do_load_state: function(state, warm) {
690         var self = this,
691             defs = [];
692         if (state.view_type && state.view_type !== this.active_view) {
693             defs.push(
694                 this.views[this.active_view].deferred.pipe(function() {
695                     return self.on_mode_switch(state.view_type, true);
696                 })
697             );
698         } 
699
700         $.when(defs).then(function() {
701             self.views[self.active_view].controller.do_load_state(state, warm);
702         });
703     },
704     display_title: function () {
705         return this.action.name;
706     }
707 });
708
709 instance.web.Sidebar = instance.web.Widget.extend({
710     init: function(parent) {
711         var self = this;
712         this._super(parent);
713         var view = this.getParent();
714         this.sections = [
715             { 'name' : 'print', 'label' : _t('Print'), },
716             { 'name' : 'files', 'label' : _t('Attachment'), },
717             { 'name' : 'other', 'label' : _t('More'), }
718         ];
719         this.items = {
720             'print' : [],
721             'files' : [],
722             'other' : []
723         };
724         if (this.session.uid === 1) {
725             var item = { label: _t("Translate"), callback: view.on_sidebar_translate, title: _t("Technical translation") };
726             this.items.other.push(item);
727         }
728         this.fileupload_id = _.uniqueId('oe_fileupload');
729         $(window).on(this.fileupload_id, function() {
730             var args = [].slice.call(arguments).slice(1);
731             if (args[0] && args[0].error) {
732                 alert(args[0].error);
733             } else {
734                 self.do_attachement_update(self.dataset, self.model_id);
735             }
736             $.unblockUI();
737         });
738     },
739     start: function() {
740         var self = this;
741         this._super(this);
742         this.redraw();
743         this.$element.on('click','.oe_dropdown_menu li a', function(event) {
744             var section = $(this).data('section');
745             var index = $(this).data('index');
746             var item = self.items[section][index];
747             if (item.callback) {
748                 item.callback.apply(self, [item]);
749             } else if (item.action) {
750                 self.on_item_action_clicked(item);
751             } else if (item.url) {
752                 return true;
753             }
754             event.preventDefault();
755         });
756     },
757     redraw: function() {
758         var self = this;
759         self.$element.html(QWeb.render('Sidebar', {widget: self}));
760
761         // Hides Sidebar sections when item list is empty
762         this.$('.oe_form_dropdown_section').each(function() {
763             $(this).toggle(!!$(this).find('li').length);
764         });
765     },
766     /**
767      * For each item added to the section:
768      *
769      * ``label``
770      *     will be used as the item's name in the sidebar, can be html
771      *
772      * ``action``
773      *     descriptor for the action which will be executed, ``action`` and
774      *     ``callback`` should be exclusive
775      *
776      * ``callback``
777      *     function to call when the item is clicked in the sidebar, called
778      *     with the item descriptor as its first argument (so information
779      *     can be stored as additional keys on the object passed to
780      *     ``add_items``)
781      *
782      * ``classname`` (optional)
783      *     ``@class`` set on the sidebar serialization of the item
784      *
785      * ``title`` (optional)
786      *     will be set as the item's ``@title`` (tooltip)
787      *
788      * @param {String} section_code
789      * @param {Array<{label, action | callback[, classname][, title]}>} items
790      */
791     add_items: function(section_code, items) {
792         var self = this;
793         if (items) {
794             this.items[section_code].push.apply(this.items[section_code],items);
795             this.redraw();
796         }
797     },
798     add_toolbar: function(toolbar) {
799         var self = this;
800         _.each(['print','action','relate'], function(type) {
801             var items = toolbar[type];
802             if (items) {
803                 for (var i = 0; i < items.length; i++) {
804                     items[i] = {
805                         label: items[i]['name'],
806                         action: items[i],
807                         classname: 'oe_sidebar_' + type
808                     }
809                 }
810                 self.add_items(type=='print' ? 'print' : 'other', items);
811             }
812         });
813     },
814     on_item_action_clicked: function(item) {
815         var self = this;
816         self.getParent().sidebar_context().then(function (context) {
817             var ids = self.getParent().get_selected_ids();
818             if (ids.length == 0) {
819                 instance.web.dialog($("<div />").text(_t("You must choose at least one record.")), { title: _t("Warning"), modal: true });
820                 return false;
821             }
822             var additional_context = _.extend({
823                 active_id: ids[0],
824                 active_ids: ids,
825                 active_model: self.getParent().dataset.model
826             }, context);
827             self.rpc("/web/action/load", {
828                 action_id: item.action.id,
829                 context: additional_context
830             }, function(result) {
831                 result.result.context = _.extend(result.result.context || {},
832                     additional_context);
833                 result.result.flags = result.result.flags || {};
834                 result.result.flags.new_window = true;
835                 self.do_action(result.result, function () {
836                     // reload view
837                     self.getParent().reload();
838                 });
839             });
840         });
841     },
842     do_attachement_update: function(dataset, model_id) {
843         this.dataset = dataset;
844         this.model_id = model_id;
845         if (!model_id) {
846             this.on_attachments_loaded([]);
847         } else {
848             var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ];
849             var ds = new instance.web.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom);
850             ds.read_slice(['name', 'url', 'type'], {}).then(this.on_attachments_loaded);
851         }
852     },
853     on_attachments_loaded: function(attachments) {
854         var self = this;
855         var items = [];
856         var prefix = this.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id=';
857         _.each(attachments,function(a) {
858             a.label = a.name;
859             if(a.type === "binary") {
860                 a.url = prefix  + a.id + '&t=' + (new Date().getTime());
861             }
862         });
863         self.items['files'] = attachments;
864         self.redraw();
865         this.$('.oe_sidebar_add_attachment .oe-binary-file').change(this.on_attachment_changed);
866         this.$element.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
867     },
868     on_attachment_changed: function(e) {
869         var $e = $(e.target);
870         if ($e.val() !== '') {
871             this.$element.find('form.oe-binary-form').submit();
872             $e.parent().find('input[type=file]').prop('disabled', true);
873             $e.parent().find('button').prop('disabled', true).find('img, span').toggle();
874             this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...'));
875             $.blockUI();
876         }
877     },
878     on_attachment_delete: function(e) {
879         var self = this;
880         e.preventDefault();
881         e.stopPropagation();
882         var self = this;
883         var $e = $(e.currentTarget);
884         if (confirm(_t("Do you really want to delete this attachment ?"))) {
885             (new instance.web.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).then(function() {
886                 self.do_attachement_update(self.dataset, self.model_id);
887             });
888         }
889     }
890 });
891
892 instance.web.TranslateDialog = instance.web.Dialog.extend({
893     dialog_title: {toString: function () { return _t("Translations"); }},
894     init: function(view) {
895         // TODO fme: should add the language to fields_view_get because between the fields view get
896         // and the moment the user opens the translation dialog, the user language could have been changed
897         this.view_language = view.session.user_context.lang;
898         this['on_button_' + _t("Save")] = this.on_btn_save;
899         this['on_button_' + _t("Close")] = this.on_btn_close;
900         this._super(view, {
901             width: '80%',
902             height: '80%'
903         });
904         this.view = view;
905         this.view_type = view.fields_view.type || '';
906         this.$fields_form = null;
907         this.$view_form = null;
908         this.$sidebar_form = null;
909         this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name });
910         this.languages = null;
911         this.languages_loaded = $.Deferred();
912         (new instance.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(),
913             [['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }).then(this.on_languages_loaded);
914     },
915     start: function() {
916         var self = this;
917         this._super();
918         $.when(this.languages_loaded).then(function() {
919             self.$element.html(instance.web.qweb.render('TranslateDialog', { widget: self }));
920             self.$fields_form = self.$element.find('.oe_translation_form');
921             self.$fields_form.find('.oe_trad_field').change(function() {
922                 $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value')));
923             });
924         });
925         return this;
926     },
927     on_languages_loaded: function(langs) {
928         this.languages = langs;
929         this.languages_loaded.resolve();
930     },
931     do_load_fields_values: function(callback) {
932         var self = this,
933             deffered = [];
934         this.$fields_form.find('.oe_trad_field').val('').removeClass('touched');
935         _.each(self.languages, function(lg) {
936             var deff = $.Deferred();
937             deffered.push(deff);
938             var callback = function(values) {
939                 _.each(self.translatable_fields_keys, function(f) {
940                     self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || '').attr('data-value', values[0][f] || '');
941                 });
942                 deff.resolve();
943             };
944             if (lg.code === self.view_language) {
945                 var values = {};
946                 _.each(self.translatable_fields_keys, function(field) {
947                     values[field] = self.view.fields[field].get_value();
948                 });
949                 callback([values]);
950             } else {
951                 self.rpc('/web/dataset/get', {
952                     model: self.view.dataset.model,
953                     ids: [self.view.datarecord.id],
954                     fields: self.translatable_fields_keys,
955                     context: self.view.dataset.get_context({
956                         'lang': lg.code
957                     })}, callback);
958             }
959         });
960         $.when.apply(null, deffered).then(callback);
961     },
962     open: function(field) {
963         var self = this,
964             sup = this._super;
965         $.when(this.languages_loaded).then(function() {
966             if (self.view.translatable_fields && self.view.translatable_fields.length) {
967                 self.do_load_fields_values(function() {
968                     sup.call(self);
969                     if (field) {
970                         var $field_input = self.$element.find('tr[data-field="' + field.name + '"] td:nth-child(2) *:first-child');
971                         self.$element.scrollTo($field_input);
972                         $field_input.focus();
973                     }
974                 });
975             } else {
976                 sup.call(self);
977             }
978         });
979     },
980     on_btn_save: function() {
981         var trads = {},
982             self = this,
983             trads_mutex = new $.Mutex();
984         self.$fields_form.find('.oe_trad_field.touched').each(function() {
985             var field = $(this).attr('name').split('-');
986             if (!trads[field[0]]) {
987                 trads[field[0]] = {};
988             }
989             trads[field[0]][field[1]] = $(this).val();
990         });
991         _.each(trads, function(data, code) {
992             if (code === self.view_language) {
993                 _.each(data, function(value, field) {
994                     self.view.fields[field].set_value(value);
995                 });
996             }
997             trads_mutex.exec(function() {
998                 return self.view.dataset.write(self.view.datarecord.id, data, { context : { 'lang': code } });
999             });
1000         });
1001         this.close();
1002     },
1003     on_btn_close: function() {
1004         this.close();
1005     }
1006 });
1007
1008 instance.web.View = instance.web.Widget.extend({
1009     template: "EmptyComponent",
1010     // name displayed in view switchers
1011     display_name: '',
1012     /**
1013      * Define a view type for each view to allow automatic call to fields_view_get.
1014      */
1015     view_type: undefined,
1016     init: function(parent, dataset, view_id, options) {
1017         this._super(parent);
1018         this.dataset = dataset;
1019         this.view_id = view_id;
1020         this.set_default_options(options);
1021     },
1022     start: function() {
1023         return this.load_view();
1024     },
1025     load_view: function() {
1026         if (this.embedded_view) {
1027             var def = $.Deferred();
1028             var self = this;
1029             $.async_when().then(function() {def.resolve(self.embedded_view);});
1030             return def.pipe(this.on_loaded);
1031         } else {
1032             var context = new instance.web.CompoundContext(this.dataset.get_context());
1033             if (! this.view_type)
1034                 console.warn("view_type is not defined", this);
1035             return this.rpc("/web/view/load", {
1036                 "model": this.dataset.model,
1037                 "view_id": this.view_id,
1038                 "view_type": this.view_type,
1039                 toolbar: !!this.options.$sidebar,
1040                 context: context
1041                 }).pipe(this.on_loaded);
1042         }
1043     },
1044     /**
1045      * Called after a successful call to fields_view_get.
1046      * Must return a promise.
1047      */
1048     on_loaded: function(fields_view_get) {
1049     },
1050     set_default_options: function(options) {
1051         this.options = options || {};
1052         _.defaults(this.options, {
1053             // All possible views options should be defaulted here
1054             $sidebar: null,
1055             sidebar_id: null,
1056             action: null,
1057             action_views_ids: {}
1058         });
1059     },
1060     open_translate_dialog: function(field) {
1061         if (!this.translate_dialog) {
1062             this.translate_dialog = new instance.web.TranslateDialog(this).start();
1063         }
1064         this.translate_dialog.open(field);
1065     },
1066     /**
1067      * Fetches and executes the action identified by ``action_data``.
1068      *
1069      * @param {Object} action_data the action descriptor data
1070      * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
1071      * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
1072      * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
1073      * @param {Object} [action_data.context=null] additional action context, to add to the current context
1074      * @param {instance.web.DataSet} dataset a dataset object used to communicate with the server
1075      * @param {Object} [record_id] the identifier of the object on which the action is to be applied
1076      * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
1077      */
1078     do_execute_action: function (action_data, dataset, record_id, on_closed) {
1079         var self = this;
1080         var result_handler = function () {
1081             if (on_closed) { on_closed.apply(null, arguments); }
1082             if (self.getParent() && self.getParent().on_action_executed) {
1083                 return self.getParent().on_action_executed.apply(null, arguments);
1084             }
1085         };
1086         var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
1087
1088         var handler = function (r) {
1089             var action = r.result;
1090             if (action && action.constructor == Object) {
1091                 var ncontext = new instance.web.CompoundContext(context);
1092                 if (record_id) {
1093                     ncontext.add({
1094                         active_id: record_id,
1095                         active_ids: [record_id],
1096                         active_model: dataset.model
1097                     });
1098                 }
1099                 ncontext.add(action.context || {});
1100                 return self.rpc('/web/session/eval_domain_and_context', {
1101                     contexts: [ncontext],
1102                     domains: []
1103                 }).pipe(function (results) {
1104                     action.context = results.context;
1105                     /* niv: previously we were overriding once more with action_data.context,
1106                      * I assumed this was not a correct behavior and removed it
1107                      */
1108                     return self.do_action(action, result_handler);
1109                 }, null);
1110             } else {
1111                 return result_handler();
1112             }
1113         };
1114
1115         if (action_data.special) {
1116             return handler({result: {"type":"ir.actions.act_window_close"}});
1117         } else if (action_data.type=="object") {
1118             var args = [[record_id]], additional_args = [];
1119             if (action_data.args) {
1120                 try {
1121                     // Warning: quotes and double quotes problem due to json and xml clash
1122                     // Maybe we should force escaping in xml or do a better parse of the args array
1123                     additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
1124                     args = args.concat(additional_args);
1125                 } catch(e) {
1126                     console.error("Could not JSON.parse arguments", action_data.args);
1127                 }
1128             }
1129             args.push(context);
1130             return dataset.call_button(action_data.name, args, handler);
1131         } else if (action_data.type=="action") {
1132             return this.rpc('/web/action/load', { action_id: parseInt(action_data.name, 10), context: context, do_not_eval: true}, handler);
1133         } else  {
1134             return dataset.exec_workflow(record_id, action_data.name, handler);
1135         }
1136     },
1137     /**
1138      * Directly set a view to use instead of calling fields_view_get. This method must
1139      * be called before start(). When an embedded view is set, underlying implementations
1140      * of instance.web.View must use the provided view instead of any other one.
1141      *
1142      * @param embedded_view A view.
1143      */
1144     set_embedded_view: function(embedded_view) {
1145         this.embedded_view = embedded_view;
1146     },
1147     do_show: function () {
1148         this.$element.show();
1149     },
1150     do_hide: function () {
1151         this.$element.hide();
1152     },
1153     do_push_state: function(state) {
1154         if (this.getParent() && this.getParent().do_push_state) {
1155             this.getParent().do_push_state(state);
1156         }
1157     },
1158     do_load_state: function(state, warm) {
1159     },
1160     /**
1161      * Switches to a specific view type
1162      *
1163      * @param {String} view view type to switch to
1164      */
1165     do_switch_view: function(view) { 
1166     },
1167     /**
1168      * Cancels the switch to the current view, switches to the previous one
1169      *
1170      * @param {Object} [options]
1171      * @param {Boolean} [options.created=false] resource was created
1172      * @param {String} [options.default=null] view to switch to if no previous view
1173      */
1174     do_prev_view: function (options) {
1175     },
1176     do_search: function(view) {
1177     },
1178     on_sidebar_import: function() {
1179         var import_view = new instance.web.DataImport(this, this.dataset);
1180         import_view.start();
1181     },
1182     on_sidebar_export: function() {
1183         var export_view = new instance.web.DataExport(this, this.dataset);
1184         export_view.start();
1185     },
1186     on_sidebar_translate: function() {
1187         return this.do_action({
1188             res_model : 'ir.translation',
1189             domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
1190             views: [[false, 'list'], [false, 'form']],
1191             type : 'ir.actions.act_window',
1192             view_type : "list",
1193             view_mode : "list"
1194         });
1195     },
1196     sidebar_context: function () {
1197         return $.when();
1198     },
1199     /**
1200      * Asks the view to reload itself, if the reloading is asynchronous should
1201      * return a {$.Deferred} indicating when the reloading is done.
1202      */
1203     reload: function () {
1204         return $.when();
1205     }
1206 });
1207
1208 instance.web.xml_to_json = function(node) {
1209     switch (node.nodeType) {
1210         case 3:
1211         case 4:
1212             return node.data;
1213         break;
1214         case 1:
1215             var attrs = $(node).getAttributes();
1216             _.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
1217                 if (attrs[key]) {
1218                     try {
1219                         attrs[key] = JSON.parse(attrs[key]);
1220                     } catch(e) { }
1221                 }
1222             });
1223             return {
1224                 tag: node.tagName.toLowerCase(),
1225                 attrs: attrs,
1226                 children: _.map(node.childNodes, instance.web.xml_to_json)
1227             }
1228     }
1229 }
1230 instance.web.json_node_to_xml = function(node, human_readable, indent) {
1231     // For debugging purpose, this function will convert a json node back to xml
1232     // Maybe useful for xml view editor
1233     indent = indent || 0;
1234     var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''),
1235         r = sindent + '<' + node.tag,
1236         cr = human_readable ? '\n' : '';
1237
1238     if (typeof(node) === 'string') {
1239         return sindent + node;
1240     } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
1241         throw("Node a json node");
1242     }
1243     for (var attr in node.attrs) {
1244         var vattr = node.attrs[attr];
1245         if (typeof(vattr) !== 'string') {
1246             // domains, ...
1247             vattr = JSON.stringify(vattr);
1248         }
1249         vattr = vattr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1250         if (human_readable) {
1251             vattr = vattr.replace(/&quot;/g, "'");
1252         }
1253         r += ' ' + attr + '="' + vattr + '"';
1254     }
1255     if (node.children && node.children.length) {
1256         r += '>' + cr;
1257         var childs = [];
1258         for (var i = 0, ii = node.children.length; i < ii; i++) {
1259             childs.push(instance.web.json_node_to_xml(node.children[i], human_readable, indent + 1));
1260         }
1261         r += childs.join(cr);
1262         r += cr + sindent + '</' + node.tag + '>';
1263         return r;
1264     } else {
1265         return r + '/>';
1266     }
1267 }
1268 instance.web.xml_to_str = function(node) {
1269     if (window.ActiveXObject) {
1270         return node.xml;
1271     } else {
1272         return (new XMLSerializer()).serializeToString(node);
1273     }
1274 }
1275 instance.web.str_to_xml = function(s) {
1276     if (window.DOMParser) {
1277         var dp = new DOMParser();
1278         var r = dp.parseFromString(s, "text/xml");
1279         if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
1280             throw new Error("Could not parse string to xml");
1281         }
1282         return r;
1283     }
1284     var xDoc;
1285     try {
1286         xDoc = new ActiveXObject("MSXML2.DOMDocument");
1287     } catch (e) {
1288         throw new Error("Could not find a DOM Parser: " + e.message);
1289     }
1290     xDoc.async = false;
1291     xDoc.preserveWhiteSpace = true;
1292     xDoc.loadXML(s);
1293     return xDoc;
1294 }
1295
1296 /**
1297  * Registry for all the main views
1298  */
1299 instance.web.views = new instance.web.Registry();
1300
1301 };
1302
1303 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: