[FIX] web: add missing icon, add warning, fix deferred
[odoo/odoo.git] / addons / web / static / src / js / views.js
1 /*---------------------------------------------------------
2  * OpenERP web library
3  *---------------------------------------------------------*/
4
5 (function() {
6 "use strict";
7
8 var instance = openerp;
9 openerp.web.views = {};
10 var QWeb = instance.web.qweb,
11     _t = instance.web._t;
12
13 instance.web.ActionManager = instance.web.Widget.extend({
14     template: "ActionManager",
15     init: function(parent) {
16         this._super(parent);
17         this.inner_action = null;
18         this.inner_widget = null;
19         this.webclient = parent;
20         this.dialog = null;
21         this.dialog_widget = null;
22         this.widgets = [];
23         this.on('history_back', this, this.proxy('history_back'));
24     },
25     dialog_stop: function (reason) {
26         if (this.dialog) {
27             this.dialog.destroy(reason);
28         }
29         this.dialog = null;
30     },
31     /**
32      * Add a new widget to the action manager
33      *
34      * widget: typically, widgets added are instance.web.ViewManager.  The action manager
35      *      uses this list of widget to handle the breadcrumbs.
36      * action: new action
37      * options.on_reverse_breadcrumb: will be called when breadcrumb is selected
38      * options.clear_breadcrumbs: boolean, if true, current widgets are destroyed
39      * options.replace_breadcrumb: boolean, if true, replace current breadcrumb
40      */
41     push_widget: function(widget, action, options) {
42         var self = this,
43             to_destroy,
44             options = options || {},
45             old_widget = this.inner_widget;
46
47         if (options.clear_breadcrumbs) {
48             to_destroy = this.widgets;
49             this.widgets = [];
50         } else if (options.replace_breadcrumb) {
51             to_destroy = _.last(this.widgets);
52             this.widgets = _.initial(this.widgets);
53         }
54         if (widget instanceof instance.web.Widget) {
55             var title = widget.get('title') || action.display_name || action.name;
56             widget.set('title', title);
57             this.widgets.push(widget);
58         } else {
59             this.widgets.push({
60                 view_stack: [{
61                     controller: {get: function () {return action.display_name || action.name; }},
62                 }],
63                 destroy: function () {},
64             });
65         }
66         _.last(this.widgets).__on_reverse_breadcrumb = options.on_reverse_breadcrumb;
67         this.inner_action = action;
68         this.inner_widget = widget;
69         return $.when(this.inner_widget.appendTo(this.$el)).done(function () {
70             (action.target !== 'inline') && (!action.flags.headless) && widget.$header && widget.$header.show();
71             old_widget && old_widget.$el.hide();
72             if (options.clear_breadcrumbs) {
73                 self.clear_widgets(to_destroy)
74             }
75         });
76     },
77     get_breadcrumbs: function () {
78         return _.flatten(_.map(this.widgets, function (widget) {
79             if (widget instanceof instance.web.ViewManager) {
80                 return widget.view_stack.map(function (view, index) { 
81                     return {
82                         title: view.controller.get('title') || widget.title,
83                         index: index,
84                         widget: widget,
85                     }; 
86                 });
87             } else {
88                 return {title: widget.get('title'), widget: widget };
89             }
90         }), true);
91     },
92     get_title: function () {
93         if (this.widgets.length === 1) {
94             // horrible hack to display the action title instead of "New" for the actions
95             // that use a form view to edit something that do not correspond to a real model
96             // for example, point of sale "Your Session" or most settings form,
97             var widget = this.widgets[0];
98             if (widget instanceof instance.web.ViewManager && widget.view_stack.length === 1) {
99                 return widget.title;
100             }
101         }
102         return _.pluck(this.get_breadcrumbs(), 'title').join(' / ');
103     },
104     get_widgets: function () {
105         return this.widgets.slice(0);
106     },
107     history_back: function() {
108         var widget = _.last(this.widgets);
109         if (widget instanceof instance.web.ViewManager) {
110             var nbr_views = widget.view_stack.length;
111             if (nbr_views > 1) {
112                 return this.select_widget(widget, nbr_views - 2);
113             } 
114         } 
115         if (this.widgets.length > 1) {
116             widget = this.widgets[this.widgets.length - 2];
117             var index = widget.view_stack && widget.view_stack.length - 1;
118             this.select_widget(widget, index);
119         }
120     },
121     select_widget: function(widget, index) {
122         var self = this;
123         if (this.webclient.has_uncommitted_changes()) {
124             return false;
125         }
126         var widget_index = this.widgets.indexOf(widget),
127             def = $.when(widget.select_view && widget.select_view(index));
128
129         def.done(function () {
130             if (widget.__on_reverse_breadcrumb) {
131                 widget.__on_reverse_breadcrumb();
132             }
133             _.each(self.widgets.splice(widget_index + 1), function (w) {
134                 w.destroy();
135             });
136             self.inner_widget = _.last(self.widgets);
137             self.inner_widget.display_breadcrumbs && self.inner_widget.display_breadcrumbs();
138             self.inner_widget.do_show && self.inner_widget.do_show();
139         });
140     },
141     clear_widgets: function(widgets) {
142         _.invoke(widgets || this.widgets, 'destroy');
143         if (!widgets) {
144             this.widgets = [];
145             this.inner_widget = null;
146         }
147     },
148     do_push_state: function(state) {
149         if (!this.webclient || !this.webclient.do_push_state || this.dialog) {
150             return;
151         }
152         state = state || {};
153         if (this.inner_action) {
154             if (this.inner_action._push_me === false) {
155                 // this action has been explicitly marked as not pushable
156                 return;
157             }
158             state.title = this.get_title(); 
159             if(this.inner_action.type == 'ir.actions.act_window') {
160                 state.model = this.inner_action.res_model;
161             }
162             if (this.inner_action.menu_id) {
163                 state.menu_id = this.inner_action.menu_id;
164             }
165             if (this.inner_action.id) {
166                 state.action = this.inner_action.id;
167             } else if (this.inner_action.type == 'ir.actions.client') {
168                 state.action = this.inner_action.tag;
169                 var params = {};
170                 _.each(this.inner_action.params, function(v, k) {
171                     if(_.isString(v) || _.isNumber(v)) {
172                         params[k] = v;
173                     }
174                 });
175                 state = _.extend(params || {}, state);
176             }
177             if (this.inner_action.context) {
178                 var active_id = this.inner_action.context.active_id;
179                 if (active_id) {
180                     state.active_id = active_id;
181                 }
182                 var active_ids = this.inner_action.context.active_ids;
183                 if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
184                     // We don't push active_ids if it's a single element array containing the active_id
185                     // This makes the url shorter in most cases.
186                     state.active_ids = this.inner_action.context.active_ids.join(',');
187                 }
188             }
189         }
190         this.webclient.do_push_state(state);
191     },
192     do_load_state: function(state, warm) {
193         var self = this,
194             action_loaded;
195         if (state.action) {
196             if (_.isString(state.action) && instance.web.client_actions.contains(state.action)) {
197                 var action_client = {
198                     type: "ir.actions.client",
199                     tag: state.action,
200                     params: state,
201                     _push_me: state._push_me,
202                 };
203                 if (warm) {
204                     this.null_action();
205                 }
206                 action_loaded = this.do_action(action_client);
207             } else {
208                 var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
209                 if (run_action) {
210                     var add_context = {};
211                     if (state.active_id) {
212                         add_context.active_id = state.active_id;
213                     }
214                     if (state.active_ids) {
215                         // The jQuery BBQ plugin does some parsing on values that are valid integers.
216                         // It means that if there's only one item, it will do parseInt() on it,
217                         // otherwise it will keep the comma seperated list as string.
218                         add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
219                             return parseInt(id, 10) || id;
220                         });
221                     } else if (state.active_id) {
222                         add_context.active_ids = [state.active_id];
223                     }
224                     add_context.params = state;
225                     if (warm) {
226                         this.null_action();
227                     }
228                     action_loaded = this.do_action(state.action, { additional_context: add_context });
229                     $.when(action_loaded || null).done(function() {
230                         instance.webclient.menu.is_bound.done(function() {
231                             if (self.inner_action && self.inner_action.id) {
232                                 instance.webclient.menu.open_action(self.inner_action.id);
233                             }
234                         });
235                     });
236                 }
237             }
238         } else if (state.model && state.id) {
239             // TODO handle context & domain ?
240             if (warm) {
241                 this.null_action();
242             }
243             var action = {
244                 res_model: state.model,
245                 res_id: state.id,
246                 type: 'ir.actions.act_window',
247                 views: [[false, 'form']]
248             };
249             action_loaded = this.do_action(action);
250         } else if (state.sa) {
251             // load session action
252             if (warm) {
253                 this.null_action();
254             }
255             action_loaded = this.rpc('/web/session/get_session_action',  {key: state.sa}).then(function(action) {
256                 if (action) {
257                     return self.do_action(action);
258                 }
259             });
260         }
261
262         $.when(action_loaded || null).done(function() {
263             if (self.inner_widget && self.inner_widget.do_load_state) {
264                 self.inner_widget.do_load_state(state, warm);
265             }
266         });
267     },
268     /**
269      * Execute an OpenERP action
270      *
271      * @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
272      * @param {Object} [options]
273      * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
274      * @param {Boolean} [options.replace_breadcrumb=false] Replace the current breadcrumb with the action
275      * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
276      * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
277      * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
278      * @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
279      * @param {Object} [options.additional_context] Additional context to be merged with the action's context.
280      * @return {jQuery.Deferred} Action loaded
281      */
282     do_action: function(action, options) {
283         options = _.defaults(options || {}, {
284             clear_breadcrumbs: false,
285             on_reverse_breadcrumb: function() {},
286             hide_breadcrumb: false,
287             on_close: function() {},
288             action_menu_id: null,
289             additional_context: {},
290         });
291
292         if (action === false) {
293             action = { type: 'ir.actions.act_window_close' };
294         } else if (_.isString(action) && instance.web.client_actions.contains(action)) {
295             var action_client = { type: "ir.actions.client", tag: action, params: {} };
296             return this.do_action(action_client, options);
297         } else if (_.isNumber(action) || _.isString(action)) {
298             var self = this;
299             var additional_context = {
300                 active_id : options.additional_context.active_id,
301                 active_ids : options.additional_context.active_ids,
302                 active_model : options.additional_context.active_model
303             };
304             return self.rpc("/web/action/load", { action_id: action, additional_context : additional_context }).then(function(result) {
305                 return self.do_action(result, options);
306             });
307         }
308
309         instance.web.bus.trigger('action', action);
310
311         // Ensure context & domain are evaluated and can be manipulated/used
312         var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {});
313         action.context = instance.web.pyeval.eval('context', ncontext);
314         if (action.context.active_id || action.context.active_ids) {
315             // Here we assume that when an `active_id` or `active_ids` is used
316             // in the context, we are in a `related` action, so we disable the
317             // searchview's default custom filters.
318             action.context.search_disable_custom_filters = true;
319         }
320         if (action.domain) {
321             action.domain = instance.web.pyeval.eval(
322                 'domain', action.domain, action.context || {});
323         }
324
325         if (!action.type) {
326             console.error("No type for action", action);
327             return $.Deferred().reject();
328         }
329         var type = action.type.replace(/\./g,'_');
330         var popup = action.target === 'new';
331         var inline = action.target === 'inline' || action.target === 'inlineview';
332         var form = _.str.startsWith(action.view_mode, 'form');
333         action.flags = _.defaults(action.flags || {}, {
334             views_switcher : !popup && !inline,
335             search_view : !popup && !inline,
336             action_buttons : !popup && !inline,
337             sidebar : !popup && !inline,
338             pager : (!popup || !form) && !inline,
339             display_title : !popup,
340             search_disable_custom_filters: action.context && action.context.search_disable_custom_filters
341         });
342         action.menu_id = options.action_menu_id;
343         if (!(type in this)) {
344             console.error("Action manager can't handle action of type " + action.type, action);
345             return $.Deferred().reject();
346         }
347         return this[type](action, options);
348     },
349     null_action: function() {
350         this.dialog_stop();
351         this.clear_widgets();
352     },
353     /**
354      *
355      * @param {Object} executor
356      * @param {Object} executor.action original action
357      * @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance
358      * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new
359      * @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
360      * @param {Object} options
361      * @return {*}
362      */
363     ir_actions_common: function(executor, options) {
364         var widget;
365         if (executor.action.target === 'new') {
366             var pre_dialog = (this.dialog && !this.dialog.isDestroyed()) ? this.dialog : null;
367             if (pre_dialog){
368                 // prevent previous dialog to consider itself closed,
369                 // right now, as we're opening a new one (prevents
370                 // reload of original form view)
371                 pre_dialog.off('closing', null, pre_dialog.on_close);
372             }
373             if (this.dialog_widget && !this.dialog_widget.isDestroyed()) {
374                 this.dialog_widget.destroy();
375             }
376             // explicitly passing a closing action to dialog_stop() prevents
377             // it from reloading the original form view
378             this.dialog_stop(executor.action);
379             this.dialog = new instance.web.Dialog(this, {
380                 title: executor.action.name,
381                 dialogClass: executor.klass,
382             });
383
384             // chain on_close triggers with previous dialog, if any
385             this.dialog.on_close = function(){
386                 options.on_close.apply(null, arguments);
387                 if (pre_dialog && pre_dialog.on_close){
388                     // no parameter passed to on_close as this will
389                     // only be called when the last dialog is truly
390                     // closing, and *should* trigger a reload of the
391                     // underlying form view (see comments above)
392                     pre_dialog.on_close();
393                 }
394             };
395             this.dialog.on("closing", null, this.dialog.on_close);
396             widget = executor.widget();
397             if (widget instanceof instance.web.ViewManager) {
398                 _.extend(widget.flags, {
399                     $buttons: this.dialog.$buttons,
400                     footer_to_buttons: true,
401                 });
402                 if (widget.action.view_mode === 'form') {
403                     widget.flags.headless = true;
404                 }
405             }
406             this.dialog_widget = widget;
407             this.dialog_widget.setParent(this.dialog);
408             var initialized = this.dialog_widget.appendTo(this.dialog.$el);
409             this.dialog.open();
410             return $.when(initialized);
411         }
412         if (this.inner_widget && this.webclient.has_uncommitted_changes()) {
413             return $.Deferred().reject();
414         }
415         widget = executor.widget();
416         this.dialog_stop(executor.action);
417         return this.push_widget(widget, executor.action, options);
418     },
419     ir_actions_act_window: function (action, options) {
420         var self = this;
421
422         return this.ir_actions_common({
423             widget: function () { 
424                 return new instance.web.ViewManager(self, null, null, null, action); 
425             },
426             action: action,
427             klass: 'oe_act_window',
428         }, options);
429     },
430     ir_actions_client: function (action, options) {
431         var self = this;
432         var ClientWidget = instance.web.client_actions.get_object(action.tag);
433         if (!ClientWidget) {
434             return self.do_warn("Action Error", "Could not find client action '" + action.tag + "'.");
435         }
436
437         if (!(ClientWidget.prototype instanceof instance.web.Widget)) {
438             var next;
439             if ((next = ClientWidget(this, action))) {
440                 return this.do_action(next, options);
441             }
442             return $.when();
443         }
444
445         return this.ir_actions_common({
446             widget: function () { return new ClientWidget(self, action); },
447             action: action,
448             klass: 'oe_act_client',
449         }, options).then(function () {
450             if (action.tag !== 'reload') {self.do_push_state({});}
451         });
452     },
453     ir_actions_act_window_close: function (action, options) {
454         if (!this.dialog) {
455             options.on_close();
456         }
457         this.dialog_stop();
458         return $.when();
459     },
460     ir_actions_server: function (action, options) {
461         var self = this;
462         this.rpc('/web/action/run', {
463             action_id: action.id,
464             context: action.context || {}
465         }).done(function (action) {
466             self.do_action(action, options);
467         });
468     },
469     ir_actions_report_xml: function(action, options) {
470         var self = this;
471         instance.web.blockUI();
472         action = _.clone(action);
473         var eval_contexts = ([instance.session.user_context] || []).concat([action.context]);
474         action.context = instance.web.pyeval.eval('contexts',eval_contexts);
475
476         // iOS devices doesn't allow iframe use the way we do it,
477         // opening a new window seems the best way to workaround
478         if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
479             var params = {
480                 action: JSON.stringify(action),
481                 token: new Date().getTime()
482             };
483             var url = self.session.url('/web/report', params);
484             instance.web.unblockUI();
485             $('<a href="'+url+'" target="_blank"></a>')[0].click();
486             return;
487         }
488         var c = instance.webclient.crashmanager;
489         return $.Deferred(function (d) {
490             self.session.get_file({
491                 url: '/web/report',
492                 data: {action: JSON.stringify(action)},
493                 complete: instance.web.unblockUI,
494                 success: function(){
495                     if (!self.dialog) {
496                         options.on_close();
497                     }
498                     self.dialog_stop();
499                     d.resolve();
500                 },
501                 error: function () {
502                     c.rpc_error.apply(c, arguments);
503                     d.reject();
504                 }
505             });
506         });
507     },
508     ir_actions_act_url: function (action) {
509         if (action.target === 'self') {
510             instance.web.redirect(action.url);
511         } else {
512             window.open(action.url, '_blank');
513         }
514         return $.when();
515     },
516 });
517
518 instance.web.ViewManager =  instance.web.Widget.extend({
519     template: "ViewManager",
520     /**
521      * @param {Object} [dataset] null object (... historical reasons)
522      * @param {Array} [views] List of [view_id, view_type]
523      * @param {Object} [flags] various boolean describing UI state
524      */
525     init: function(parent, dataset, views, flags, action) {
526         if (action) {
527             var flags = action.flags || {};
528             if (!('auto_search' in flags)) {
529                 flags.auto_search = action.auto_search !== false;
530             }
531             if (action.res_model === 'board.board' && action.view_mode === 'form') {
532                 action.target = 'inline';
533                 // Special case for Dashboards
534                 _.extend(flags, {
535                     views_switcher : false,
536                     display_title : false,
537                     search_view : false,
538                     pager : false,
539                     sidebar : false,
540                     action_buttons : false
541                 });
542             }
543             this.action = action;
544             this.action_manager = parent;
545             var dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain);
546             if (action.res_id) {
547                 dataset.ids.push(action.res_id);
548                 dataset.index = 0;
549             }
550             views = action.views;
551         }
552         var self = this;
553         this._super(parent);
554
555         this.flags = flags || {};
556         this.dataset = dataset;
557         this.view_order = [];
558         this.url_states = {};
559         this.views = {};
560         this.view_stack = []; // used for breadcrumbs
561         this.active_view = null;
562         this.searchview = null;
563         this.active_search = null;
564         this.registry = instance.web.views;
565         this.title = this.action && this.action.name;
566
567         _.each(views, function (view) {
568             var view_type = view[1] || view.view_type,
569                 View = instance.web.views.get_object(view_type, true),
570                 view_label = View ? View.prototype.display_name: (void 'nope'),
571                 view = {
572                     controller: null,
573                     options: view.options || {},
574                     view_id: view[0] || view.view_id,
575                     type: view_type,
576                     label: view_label,
577                     embedded_view: view.embedded_view,
578                     title: self.action && self.action.name,
579                     button_label: View ? _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}) : (void 'nope'),
580                 };
581             self.view_order.push(view);
582             self.views[view_type] = view;
583         });
584         this.multiple_views = (self.view_order.length - ('form' in this.views ? 1 : 0)) > 1;
585     },
586     /**
587      * @returns {jQuery.Deferred} initial view loading promise
588      */
589     start: function() {
590         var self = this;
591         var default_view = this.flags.default_view || this.view_order[0].type,
592             default_options = this.flags[default_view] && this.flags[default_view].options;
593
594         if (this.flags.headless) {
595             this.$('.oe-view-manager-header').hide();
596         }
597         this._super();
598         var $sidebar = this.flags.sidebar ? this.$('.oe-view-manager-sidebar') : undefined,
599             $pager = this.$('.oe-view-manager-pager');
600
601         this.$breadcrumbs = this.$('.oe-view-title');
602         this.$switch_buttons = this.$('.oe-view-manager-switch button');
603         this.$header = this.$('.oe-view-manager-header');
604         this.$header_col = this.$header.find('.oe-header-title');
605         this.$search_col = this.$header.find('.oe-view-manager-search-view');
606         this.$switch_buttons.click(function (event) {
607             if (!$(event.target).hasClass('active')) {
608                 self.switch_mode($(this).data('view-type'));
609             }
610         });
611         var views_ids = {};
612         _.each(this.views, function (view) {
613             views_ids[view.type] = view.view_id;
614             view.options = _.extend({
615                 $buttons: self.$('.oe-' + view.type + '-buttons'),
616                 $sidebar : $sidebar,
617                 $pager : $pager,
618                 action : self.action,
619                 action_views_ids : views_ids,
620             }, self.flags, self.flags[view.type], view.options);
621             if (view.type !== 'form') {
622                 self.$('.oe-vm-switch-' + view.type).tooltip();
623             }
624         });
625         this.$('.oe_debug_view').click(this.on_debug_changed);
626         this.$el.addClass("oe_view_manager_" + ((this.action && this.action.target) || 'current'));
627
628         this.search_view_loaded = this.setup_search_view();
629         var main_view_loaded = this.switch_mode(default_view, null, default_options);
630             
631         return $.when(main_view_loaded, this.search_view_loaded);
632     },
633
634     switch_mode: function(view_type, no_store, view_options) {
635         var self = this,
636             view = this.views[view_type];
637
638         if (!view) {
639             return $.Deferred().reject();
640         }
641         if ((view_type !== 'form') && (view_type !== 'diagram')) {
642             this.view_stack = [];
643         } 
644
645         this.view_stack.push(view);
646         this.active_view = view;
647         if (!view.created) {
648             view.created = this.create_view.bind(this)(view);
649         }
650         this.active_search = $.Deferred();
651
652         if (this.searchview
653                 && this.flags.auto_search
654                 && view.controller.searchable !== false) {
655             $.when(this.search_view_loaded, view.created).done(this.searchview.do_search);
656         } else {
657             this.active_search.resolve();
658         }
659
660         self.update_header();
661         return $.when(view.created, this.active_search).done(function () {
662             self.active_view = view;
663             self._display_view(view.type, view_options);
664             self.trigger('switch_mode', view_type, no_store, view_options);
665             if (self.session.debug) {
666                 self.$('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
667                     view: self.active_view.controller,
668                     view_manager: self,
669                 }));
670             }
671         });
672     },
673     update_header: function () {
674         this.$switch_buttons.removeClass('active');
675         this.$('.oe-vm-switch-' + this.active_view.type).addClass('active');
676     },
677     _display_view: function (view_type, view_options) {
678         var self = this;
679         this.active_view.$container.show();
680         $.when(this.active_view.controller.do_show(view_options)).done(function () { 
681             _.each(self.views, function (view) {
682                 if (view.type !== view_type) {
683                     view.controller && view.controller.do_hide();
684                     view.$container && view.$container.hide();
685                     view.options.$buttons && view.options.$buttons.hide();
686                 }
687             });
688             self.active_view.options.$buttons && self.active_view.options.$buttons.show();
689             if (self.searchview) {
690                 var is_hidden = self.active_view.controller.searchable === false;
691                 self.searchview.toggle_visibility(!is_hidden);
692                 self.$header_col.toggleClass('col-md-6', !is_hidden).toggleClass('col-md-12', is_hidden);
693                 self.$search_col.toggle(!is_hidden);
694             }
695             self.display_breadcrumbs();
696         });
697     },
698     display_breadcrumbs: function () {
699         var self = this;
700         if (!this.action_manager) return;
701         var breadcrumbs = this.action_manager.get_breadcrumbs();
702         if (!breadcrumbs.length) return;
703         var $breadcrumbs = _.map(_.initial(breadcrumbs), function (bc) {
704             var $link = $('<a>').text(bc.title);
705             $link.click(function () {
706                 self.action_manager.select_widget(bc.widget, bc.index);
707             });
708             return $('<li>').append($link);
709         });
710         $breadcrumbs.push($('<li>').addClass('active').text(_.last(breadcrumbs).title));
711         this.$breadcrumbs
712             .empty()
713             .append($breadcrumbs);
714     },
715     create_view: function(view) {
716         var self = this,
717             View = this.registry.get_object(view.type),
718             options = _.clone(view.options),
719             view_loaded = $.Deferred();
720
721         if (view.type === "form" && this.action && (this.action.target === 'new' || this.action.target === 'inline')) {
722             options.initial_mode = 'edit';
723         }
724         var controller = new View(this, this.dataset, view.view_id, options),
725             $container = this.$(".oe-view-manager-view-" + view.type + ":first");
726
727         $container.hide();
728         view.controller = controller;
729         view.$container = $container;
730
731         if (view.embedded_view) {
732             controller.set_embedded_view(view.embedded_view);
733         }
734         controller.on('switch_mode', this, this.switch_mode.bind(this));
735         controller.on('history_back', this, function () {
736             self.action_manager && self.action_manager.trigger('history_back');
737         });
738         controller.on("change:title", this, function() {
739             self.display_breadcrumbs();
740         });
741         controller.on('view_loaded', this, function () {
742             view_loaded.resolve();
743         });
744         this.$('.oe-view-manager-pager > span').hide();
745         return $.when(controller.appendTo($container), view_loaded)
746                 .done(function () { 
747                     self.trigger("controller_inited", view.type, controller);
748                 });
749     },
750     select_view: function (index) {
751         var view_type = this.view_stack[index].type;
752         this.view_stack.splice(index);
753         return this.switch_mode(view_type);
754     },
755     /**
756      * @returns {Number|Boolean} the view id of the given type, false if not found
757      */
758     get_view_id: function(view_type) {
759         return this.views[view_type] && this.views[view_type].view_id || false;
760     },    
761     /**
762      * Sets up the current viewmanager's search view.
763      *
764      * @param {Number|false} view_id the view to use or false for a default one
765      * @returns {jQuery.Deferred} search view startup deferred
766      */
767     setup_search_view: function() {
768         if (this.searchview) {
769             this.searchview.destroy();
770         }
771
772         var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
773
774         var search_defaults = {};
775
776         var context = this.action ? this.action.context : [];
777         _.each(context, function (value, key) {
778             var match = /^search_default_(.*)$/.exec(key);
779             if (match) {
780                 search_defaults[match[1]] = value;
781             }
782         });
783
784
785         var options = {
786             hidden: this.flags.search_view === false,
787             disable_custom_filters: this.flags.search_disable_custom_filters,
788             $buttons: this.$('.oe-search-options'),
789             action: this.action,
790         };
791         var SearchView = instance.web.SearchView;
792         this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options);
793
794         this.searchview.on('search_data', this, this.search.bind(this));
795         return this.searchview.appendTo(this.$(".oe-view-manager-search-view:first"));
796     },
797     search: function(domains, contexts, groupbys) {
798         var self = this,
799             controller = this.active_view.controller,
800             action_context = this.action.context || {};
801         instance.web.pyeval.eval_domains_and_contexts({
802             domains: [this.action.domain || []].concat(domains || []),
803             contexts: [action_context].concat(contexts || []),
804             group_by_seq: groupbys || []
805         }).done(function (results) {
806             if (results.error) {
807                 throw new Error(
808                         _.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s",
809                                       JSON.stringify(results.error)));
810             }
811             self.dataset._model = new instance.web.Model(
812                 self.dataset.model, results.context, results.domain);
813             var groupby = results.group_by.length
814                         ? results.group_by
815                         : action_context.group_by;
816             if (_.isString(groupby)) {
817                 groupby = [groupby];
818             }
819             $.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
820                 self.active_search.resolve();
821             });
822         });
823     },
824     do_push_state: function(state) {
825         if (this.action_manager) {
826             state.view_type = this.active_view.type;
827             this.action_manager.do_push_state(state);
828         }
829     },    
830     do_load_state: function(state, warm) {
831         if (state.view_type && state.view_type !== this.active_view.type) {
832             // warning: this code relies on the fact that switch_mode has an immediate side
833             // effect (setting the 'active_view' to its new value) AND an async effect (the
834             // view is created/loaded).  So, the next statement (do_load_state) is executed 
835             // on the new view, after it was initialized, but before it is fully loaded and 
836             // in particular, before the do_show method is called.
837             this.switch_mode(state.view_type, true);
838         } 
839         this.active_view.controller.do_load_state(state, warm);
840     },
841     on_debug_changed: function (evt) {
842         var self = this,
843             params = $(evt.target).data(),
844             val = params.action,
845             current_view = this.active_view.controller;
846         switch (val) {
847             case 'fvg':
848                 var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get") }).open();
849                 $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$el);
850                 break;
851             case 'tests':
852                 this.do_action({
853                     name: _t("JS Tests"),
854                     target: 'new',
855                     type : 'ir.actions.act_url',
856                     url: '/web/tests?mod=*'
857                 });
858                 break;
859             case 'get_metadata':
860                 var ids = current_view.get_selected_ids();
861                 if (ids.length === 1) {
862                     this.dataset.call('get_metadata', [ids]).done(function(result) {
863                         var dialog = new instance.web.Dialog(this, {
864                             title: _.str.sprintf(_t("Metadata (%s)"), self.dataset.model),
865                             size: 'medium',
866                             buttons: {
867                                 Ok: function() { this.parents('.modal').modal('hide');}
868                             },
869                         }, QWeb.render('ViewManagerDebugViewLog', {
870                             perm : result[0],
871                             format : instance.web.format_value
872                         })).open();
873                     });
874                 }
875                 break;
876             case 'toggle_layout_outline':
877                 current_view.rendering_engine.toggle_layout_debugging();
878                 break;
879             case 'set_defaults':
880                 current_view.open_defaults_dialog();
881                 break;
882             case 'translate':
883                 this.do_action({
884                     name: _t("Technical Translation"),
885                     res_model : 'ir.translation',
886                     domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
887                     views: [[false, 'list'], [false, 'form']],
888                     type : 'ir.actions.act_window',
889                     view_type : "list",
890                     view_mode : "list"
891                 });
892                 break;
893             case 'fields':
894                 this.dataset.call('fields_get', [false, {}]).done(function (fields) {
895                     var $root = $('<dl>');
896                     _(fields).each(function (attributes, name) {
897                         $root.append($('<dt>').append($('<h4>').text(name)));
898                         var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root));
899                         _(attributes).each(function (def, name) {
900                             if (def instanceof Object) {
901                                 def = JSON.stringify(def);
902                             }
903                             $attrs
904                                 .append($('<dt>').text(name))
905                                 .append($('<dd style="white-space: pre-wrap;">').text(def));
906                         });
907                     });
908                     new instance.web.Dialog(self, {
909                         title: _.str.sprintf(_t("Model %s fields"),
910                                              self.dataset.model),
911                         buttons: {
912                             Ok: function() { this.parents('.modal').modal('hide');}
913                         },
914                         }, $root).open();
915                 });
916                 break;
917             case 'edit_workflow':
918                 return this.do_action({
919                     res_model : 'workflow',
920                     name: _t('Edit Workflow'),
921                     domain : [['osv', '=', this.dataset.model]],
922                     views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
923                     type : 'ir.actions.act_window',
924                     view_type : 'list',
925                     view_mode : 'list'
926                 });
927             case 'edit':
928                 this.do_edit_resource(params.model, params.id, evt.target.text);
929                 break;
930             case 'manage_filters':
931                 this.do_action({
932                     res_model: 'ir.filters',
933                     name: _t('Manage Filters'),
934                     views: [[false, 'list'], [false, 'form']],
935                     type: 'ir.actions.act_window',
936                     context: {
937                         search_default_my_filters: true,
938                         search_default_model_id: this.dataset.model
939                     }
940                 });
941                 break;
942             case 'print_workflow':
943                 if (current_view.get_selected_ids  && current_view.get_selected_ids().length == 1) {
944                     instance.web.blockUI();
945                     var action = {
946                         context: { active_ids: current_view.get_selected_ids() },
947                         report_name: "workflow.instance.graph",
948                         datas: {
949                             model: this.dataset.model,
950                             id: current_view.get_selected_ids()[0],
951                             nested: true,
952                         }
953                     };
954                     this.session.get_file({
955                         url: '/web/report',
956                         data: {action: JSON.stringify(action)},
957                         complete: instance.web.unblockUI
958                     });
959                 } else {
960                     self.do_warn("Warning", "No record selected.");
961                 }
962                 break;
963             case 'leave_debug':
964                 window.location.search="?";
965                 break;
966             default:
967                 if (val) {
968                     console.warn("No debug handler for ", val);
969                 }
970         }
971     },
972     do_edit_resource: function(model, id, name) {
973         this.do_action({
974             res_model : model,
975             res_id : id,
976             name: name,
977             type : 'ir.actions.act_window',
978             view_type : 'form',
979             view_mode : 'form',
980             views : [[false, 'form']],
981             target : 'new',
982             flags : {
983                 action_buttons : true,
984                 headless: true,
985             }
986         });
987     },
988 });
989
990 instance.web.Sidebar = instance.web.Widget.extend({
991     init: function(parent) {
992         var self = this;
993         this._super(parent);
994         var view = this.getParent();
995         this.sections = [
996             { 'name' : 'print', 'label' : _t('Print'), },
997             { 'name' : 'other', 'label' : _t('More'), }
998         ];
999         this.items = {
1000             'print' : [],
1001             'other' : []
1002         };
1003         this.fileupload_id = _.uniqueId('oe_fileupload');
1004         $(window).on(this.fileupload_id, function() {
1005             var args = [].slice.call(arguments).slice(1);
1006             self.do_attachement_update(self.dataset, self.model_id,args);
1007             instance.web.unblockUI();
1008         });
1009     },
1010     start: function() {
1011         var self = this;
1012         this._super(this);
1013         this.redraw();
1014         this.$el.on('click','.dropdown-menu li a', function(event) {
1015             var section = $(this).data('section');
1016             var index = $(this).data('index');
1017             var item = self.items[section][index];
1018             if (item.callback) {
1019                 item.callback.apply(self, [item]);
1020             } else if (item.action) {
1021                 self.on_item_action_clicked(item);
1022             } else if (item.url) {
1023                 return true;
1024             }
1025             event.preventDefault();
1026         });
1027     },
1028     redraw: function() {
1029         var self = this;
1030         self.$el.html(QWeb.render('Sidebar', {widget: self}));
1031
1032         // Hides Sidebar sections when item list is empty
1033         this.$('.oe_form_dropdown_section').each(function() {
1034             $(this).toggle(!!$(this).find('li').length);
1035         });
1036         self.$("[title]").tooltip({
1037             delay: { show: 500, hide: 0}
1038         });
1039     },
1040     /**
1041      * For each item added to the section:
1042      *
1043      * ``label``
1044      *     will be used as the item's name in the sidebar, can be html
1045      *
1046      * ``action``
1047      *     descriptor for the action which will be executed, ``action`` and
1048      *     ``callback`` should be exclusive
1049      *
1050      * ``callback``
1051      *     function to call when the item is clicked in the sidebar, called
1052      *     with the item descriptor as its first argument (so information
1053      *     can be stored as additional keys on the object passed to
1054      *     ``add_items``)
1055      *
1056      * ``classname`` (optional)
1057      *     ``@class`` set on the sidebar serialization of the item
1058      *
1059      * ``title`` (optional)
1060      *     will be set as the item's ``@title`` (tooltip)
1061      *
1062      * @param {String} section_code
1063      * @param {Array<{label, action | callback[, classname][, title]}>} items
1064      */
1065     add_items: function(section_code, items) {
1066         var self = this;
1067         if (items) {
1068             this.items[section_code].unshift.apply(this.items[section_code],items);
1069             this.redraw();
1070         }
1071     },
1072     add_toolbar: function(toolbar) {
1073         var self = this;
1074         _.each(['print','action','relate'], function(type) {
1075             var items = toolbar[type];
1076             if (items) {
1077                 for (var i = 0; i < items.length; i++) {
1078                     items[i] = {
1079                         label: items[i]['name'],
1080                         action: items[i],
1081                         classname: 'oe_sidebar_' + type
1082                     };
1083                 }
1084                 self.add_items(type=='print' ? 'print' : 'other', items);
1085             }
1086         });
1087     },
1088     on_item_action_clicked: function(item) {
1089         var self = this;
1090         self.getParent().sidebar_eval_context().done(function (sidebar_eval_context) {
1091             var ids = self.getParent().get_selected_ids();
1092             var domain;
1093             if (self.getParent().get_active_domain) {
1094                 domain = self.getParent().get_active_domain();
1095             }
1096             else {
1097                 domain = $.Deferred().resolve(undefined);
1098             }
1099             if (ids.length === 0) {
1100                 new instance.web.Dialog(this, { title: _t("Warning"), size: 'medium',}, $("<div />").text(_t("You must choose at least one record."))).open();
1101                 return false;
1102             }
1103             var active_ids_context = {
1104                 active_id: ids[0],
1105                 active_ids: ids,
1106                 active_model: self.getParent().dataset.model,
1107             };
1108
1109             $.when(domain).done(function (domain) {
1110                 if (domain !== undefined) {
1111                     active_ids_context.active_domain = domain;
1112                 }
1113                 var c = instance.web.pyeval.eval('context',
1114                 new instance.web.CompoundContext(
1115                     sidebar_eval_context, active_ids_context));
1116
1117                 self.rpc("/web/action/load", {
1118                     action_id: item.action.id,
1119                     context: c
1120                 }).done(function(result) {
1121                     result.context = new instance.web.CompoundContext(
1122                         result.context || {}, active_ids_context)
1123                             .set_eval_context(c);
1124                     result.flags = result.flags || {};
1125                     result.flags.new_window = true;
1126                     self.do_action(result, {
1127                         on_close: function() {
1128                             // reload view
1129                             self.getParent().reload();
1130                         },
1131                     });
1132                 });
1133             });
1134         });
1135     },
1136     do_attachement_update: function(dataset, model_id, args) {
1137         var self = this;
1138         this.dataset = dataset;
1139         this.model_id = model_id;
1140         if (args && args[0].error) {
1141             this.do_warn(_t('Uploading Error'), args[0].error);
1142         }
1143         if (!model_id) {
1144             this.on_attachments_loaded([]);
1145         } else {
1146             var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ];
1147             var ds = new instance.web.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom);
1148             ds.read_slice(['name', 'url', 'type', 'create_uid', 'create_date', 'write_uid', 'write_date'], {}).done(this.on_attachments_loaded);
1149         }
1150     },
1151     on_attachments_loaded: function(attachments) {
1152         var self = this;
1153         var items = [];
1154         var prefix = this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'});
1155         _.each(attachments,function(a) {
1156             a.label = a.name;
1157             if(a.type === "binary") {
1158                 a.url = prefix  + '&id=' + a.id + '&t=' + (new Date().getTime());
1159             }
1160         });
1161         self.items.files = attachments;
1162         self.redraw();
1163         this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed);
1164         this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
1165     },
1166     on_attachment_changed: function(e) {
1167         var $e = $(e.target);
1168         if ($e.val() !== '') {
1169             this.$el.find('form.oe_form_binary_form').submit();
1170             $e.parent().find('input[type=file]').prop('disabled', true);
1171             $e.parent().find('button').prop('disabled', true).find('img, span').toggle();
1172             this.$('.oe_sidebar_add_attachment a').text(_t('Uploading...'));
1173             instance.web.blockUI();
1174         }
1175     },
1176     on_attachment_delete: function(e) {
1177         e.preventDefault();
1178         e.stopPropagation();
1179         var self = this;
1180         var $e = $(e.currentTarget);
1181         if (confirm(_t("Do you really want to delete this attachment ?"))) {
1182             (new instance.web.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).done(function() {
1183                 self.do_attachement_update(self.dataset, self.model_id);
1184             });
1185         }
1186     }
1187 });
1188
1189 instance.web.View = instance.web.Widget.extend({
1190     // name displayed in view switchers
1191     display_name: '',
1192     /**
1193      * Define a view type for each view to allow automatic call to fields_view_get.
1194      */
1195     view_type: undefined,
1196     init: function(parent, dataset, view_id, options) {
1197         this._super(parent);
1198         this.ViewManager = parent;
1199         this.dataset = dataset;
1200         this.view_id = view_id;
1201         this.set_default_options(options);
1202     },
1203     start: function () {
1204         return this.load_view();
1205     },
1206     load_view: function(context) {
1207         var self = this;
1208         var view_loaded_def;
1209         if (this.embedded_view) {
1210             view_loaded_def = $.Deferred();
1211             $.async_when().done(function() {
1212                 view_loaded_def.resolve(self.embedded_view);
1213             });
1214         } else {
1215             if (! this.view_type)
1216                 console.warn("view_type is not defined", this);
1217             view_loaded_def = instance.web.fields_view_get({
1218                 "model": this.dataset._model,
1219                 "view_id": this.view_id,
1220                 "view_type": this.view_type,
1221                 "toolbar": !!this.options.$sidebar,
1222                 "context": this.dataset.get_context(),
1223             });
1224         }
1225         return this.alive(view_loaded_def).then(function(r) {
1226             self.fields_view = r;
1227             // add css classes that reflect the (absence of) access rights
1228             self.$el.addClass('oe_view')
1229                 .toggleClass('oe_cannot_create', !self.is_action_enabled('create'))
1230                 .toggleClass('oe_cannot_edit', !self.is_action_enabled('edit'))
1231                 .toggleClass('oe_cannot_delete', !self.is_action_enabled('delete'));
1232             return $.when(self.view_loading(r)).then(function() {
1233                 self.trigger('view_loaded', r);
1234             });
1235         });
1236     },
1237     view_loading: function(r) {
1238     },
1239     set_default_options: function(options) {
1240         this.options = options || {};
1241         _.defaults(this.options, {
1242             // All possible views options should be defaulted here
1243             $sidebar: null,
1244             sidebar_id: null,
1245             action: null,
1246             action_views_ids: {}
1247         });
1248     },
1249     /**
1250      * Fetches and executes the action identified by ``action_data``.
1251      *
1252      * @param {Object} action_data the action descriptor data
1253      * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
1254      * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
1255      * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
1256      * @param {Object} [action_data.context=null] additional action context, to add to the current context
1257      * @param {instance.web.DataSet} dataset a dataset object used to communicate with the server
1258      * @param {Object} [record_id] the identifier of the object on which the action is to be applied
1259      * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
1260      */
1261     do_execute_action: function (action_data, dataset, record_id, on_closed) {
1262         var self = this;
1263         var result_handler = function () {
1264             if (on_closed) { on_closed.apply(null, arguments); }
1265             if (self.getParent() && self.getParent().on_action_executed) {
1266                 return self.getParent().on_action_executed.apply(null, arguments);
1267             }
1268         };
1269         var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
1270
1271         // response handler
1272         var handler = function (action) {
1273             if (action && action.constructor == Object) {
1274                 // filter out context keys that are specific to the current action.
1275                 // Wrong default_* and search_default_* values will no give the expected result
1276                 // Wrong group_by values will simply fail and forbid rendering of the destination view
1277                 var ncontext = new instance.web.CompoundContext(
1278                     _.object(_.reject(_.pairs(dataset.get_context().eval()), function(pair) {
1279                       return pair[0].match('^(?:(?:default_|search_default_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
1280                     }))
1281                 );
1282                 ncontext.add(action_data.context || {});
1283                 ncontext.add({active_model: dataset.model});
1284                 if (record_id) {
1285                     ncontext.add({
1286                         active_id: record_id,
1287                         active_ids: [record_id],
1288                     });
1289                 }
1290                 ncontext.add(action.context || {});
1291                 action.context = ncontext;
1292                 return self.do_action(action, {
1293                     on_close: result_handler,
1294                 });
1295             } else {
1296                 self.do_action({"type":"ir.actions.act_window_close"});
1297                 return result_handler();
1298             }
1299         };
1300
1301         if (action_data.special === 'cancel') {
1302             return handler({"type":"ir.actions.act_window_close"});
1303         } else if (action_data.type=="object") {
1304             var args = [[record_id]];
1305             if (action_data.args) {
1306                 try {
1307                     // Warning: quotes and double quotes problem due to json and xml clash
1308                     // Maybe we should force escaping in xml or do a better parse of the args array
1309                     var additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
1310                     args = args.concat(additional_args);
1311                 } catch(e) {
1312                     console.error("Could not JSON.parse arguments", action_data.args);
1313                 }
1314             }
1315             args.push(context);
1316             return dataset.call_button(action_data.name, args).then(handler).then(function () {
1317                 if (instance.webclient) {
1318                     instance.webclient.menu.do_reload_needaction();
1319                 }
1320             });
1321         } else if (action_data.type=="action") {
1322             return this.rpc('/web/action/load', {
1323                 action_id: action_data.name,
1324                 context: _.extend(instance.web.pyeval.eval('context', context), {'active_model': dataset.model, 'active_ids': dataset.ids, 'active_id': record_id}),
1325                 do_not_eval: true
1326             }).then(handler);
1327         } else  {
1328             return dataset.exec_workflow(record_id, action_data.name).then(handler);
1329         }
1330     },
1331     /**
1332      * Directly set a view to use instead of calling fields_view_get. This method must
1333      * be called before start(). When an embedded view is set, underlying implementations
1334      * of instance.web.View must use the provided view instead of any other one.
1335      *
1336      * @param embedded_view A view.
1337      */
1338     set_embedded_view: function(embedded_view) {
1339         this.embedded_view = embedded_view;
1340     },
1341     do_show: function () {
1342         this.$el.show();
1343         instance.web.bus.trigger('view_shown', this);
1344     },
1345     do_hide: function () {
1346         this.$el.hide();
1347     },
1348     is_active: function () {
1349         return this.ViewManager.active_view.controller === this;
1350     }, /**
1351      * Wraps fn to only call it if the current view is the active one. If the
1352      * current view is not active, doesn't call fn.
1353      *
1354      * fn can not return anything, as a non-call to fn can't return anything
1355      * either
1356      *
1357      * @param {Function} fn function to wrap in the active guard
1358      */
1359     guard_active: function (fn) {
1360         var self = this;
1361         return function () {
1362             if (self.is_active()) {
1363                 fn.apply(self, arguments);
1364             }
1365         };
1366     },
1367     do_push_state: function(state) {
1368         if (this.getParent() && this.getParent().do_push_state) {
1369             this.getParent().do_push_state(state);
1370         }
1371     },
1372     do_load_state: function (state, warm) {
1373
1374     },
1375     /**
1376      * Switches to a specific view type
1377      */
1378     do_switch_view: function() {
1379         this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments)));
1380     },
1381     do_search: function(domain, context, group_by) {
1382     },
1383     on_sidebar_export: function() {
1384         new instance.web.DataExport(this, this.dataset).open();
1385     },
1386     sidebar_eval_context: function () {
1387         return $.when({});
1388     },
1389     /**
1390      * Asks the view to reload itself, if the reloading is asynchronous should
1391      * return a {$.Deferred} indicating when the reloading is done.
1392      */
1393     reload: function () {
1394         return $.when();
1395     },
1396     /**
1397      * Return whether the user can perform the action ('create', 'edit', 'delete') in this view.
1398      * An action is disabled by setting the corresponding attribute in the view's main element,
1399      * like: <form string="" create="false" edit="false" delete="false">
1400      */
1401     is_action_enabled: function(action) {
1402         var attrs = this.fields_view.arch.attrs;
1403         return (action in attrs) ? JSON.parse(attrs[action]) : true;
1404     },
1405 });
1406
1407 /**
1408  * Performs a fields_view_get and apply postprocessing.
1409  * return a {$.Deferred} resolved with the fvg
1410  *
1411  * @param {Object} args
1412  * @param {String|Object} args.model instance.web.Model instance or string repr of the model
1413  * @param {Object} [args.context] context if args.model is a string
1414  * @param {Number} [args.view_id] id of the view to be loaded, default view if null
1415  * @param {String} [args.view_type] type of view to be loaded if view_id is null
1416  * @param {Boolean} [args.toolbar=false] get the toolbar definition
1417  */
1418 instance.web.fields_view_get = function(args) {
1419     function postprocess(fvg) {
1420         var doc = $.parseXML(fvg.arch).documentElement;
1421         fvg.arch = instance.web.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
1422         if ('id' in fvg.fields) {
1423             // Special case for id's
1424             var id_field = fvg.fields['id'];
1425             id_field.original_type = id_field.type;
1426             id_field.type = 'id';
1427         }
1428         _.each(fvg.fields, function(field) {
1429             _.each(field.views || {}, function(view) {
1430                 postprocess(view);
1431             });
1432         });
1433         return fvg;
1434     }
1435     args = _.defaults(args, {
1436         toolbar: false,
1437     });
1438     var model = args.model;
1439     if (typeof model === 'string') {
1440         model = new instance.web.Model(args.model, args.context);
1441     }
1442     return args.model.call('fields_view_get', {
1443         view_id: args.view_id,
1444         view_type: args.view_type,
1445         context: args.context,
1446         toolbar: args.toolbar
1447     }).then(function(fvg) {
1448         return postprocess(fvg);
1449     });
1450 };
1451
1452 instance.web.xml_to_json = function(node, strip_whitespace) {
1453     switch (node.nodeType) {
1454         case 9:
1455             return instance.web.xml_to_json(node.documentElement, strip_whitespace);
1456         case 3:
1457         case 4:
1458             return (strip_whitespace && node.data.trim() === '') ? undefined : node.data;
1459         case 1:
1460             var attrs = $(node).getAttributes();
1461             _.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
1462                 if (attrs[key]) {
1463                     try {
1464                         attrs[key] = JSON.parse(attrs[key]);
1465                     } catch(e) { }
1466                 }
1467             });
1468             return {
1469                 tag: node.tagName.toLowerCase(),
1470                 attrs: attrs,
1471                 children: _.compact(_.map(node.childNodes, function(node) {
1472                     return instance.web.xml_to_json(node, strip_whitespace);
1473                 })),
1474             };
1475     }
1476 };
1477
1478 instance.web.json_node_to_xml = function(node, human_readable, indent) {
1479     // For debugging purpose, this function will convert a json node back to xml
1480     indent = indent || 0;
1481     var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''),
1482         r = sindent + '<' + node.tag,
1483         cr = human_readable ? '\n' : '';
1484
1485     if (typeof(node) === 'string') {
1486         return sindent + node;
1487     } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
1488         throw new Error(
1489             _.str.sprintf(_t("Node [%s] is not a JSONified XML node"),
1490                           JSON.stringify(node)));
1491     }
1492     for (var attr in node.attrs) {
1493         var vattr = node.attrs[attr];
1494         if (typeof(vattr) !== 'string') {
1495             // domains, ...
1496             vattr = JSON.stringify(vattr);
1497         }
1498         vattr = vattr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1499         if (human_readable) {
1500             vattr = vattr.replace(/&quot;/g, "'");
1501         }
1502         r += ' ' + attr + '="' + vattr + '"';
1503     }
1504     if (node.children && node.children.length) {
1505         r += '>' + cr;
1506         var childs = [];
1507         for (var i = 0, ii = node.children.length; i < ii; i++) {
1508             childs.push(instance.web.json_node_to_xml(node.children[i], human_readable, indent + 1));
1509         }
1510         r += childs.join(cr);
1511         r += cr + sindent + '</' + node.tag + '>';
1512         return r;
1513     } else {
1514         return r + '/>';
1515     }
1516 };
1517 instance.web.xml_to_str = function(node) {
1518     var str = "";
1519     if (window.XMLSerializer) {
1520         str = (new XMLSerializer()).serializeToString(node);
1521     } else if (window.ActiveXObject) {
1522         str = node.xml;
1523     } else {
1524         throw new Error(_t("Could not serialize XML"));
1525     }
1526     // Browsers won't deal with self closing tags except void elements:
1527     // http://www.w3.org/TR/html-markup/syntax.html
1528     var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
1529
1530     // The following regex is a bit naive but it's ok for the xmlserializer output
1531     str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
1532         if (void_elements.indexOf(tag) < 0) {
1533             return "<" + tag + attrs + "></" + tag + ">";
1534         } else {
1535             return match;
1536         }
1537     });
1538     return str;
1539 };
1540
1541 /**
1542  * Registry for all the main views
1543  */
1544 instance.web.views = new instance.web.Registry();
1545
1546 })();
1547
1548 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: