[MERGE] Sync with trunk
[odoo/odoo.git] / addons / web / static / src / js / view_list.js
1 (function() {
2
3 var instance = openerp;
4 openerp.web.list = {};
5 var _t = instance.web._t,
6     _lt = instance.web._lt;
7 var QWeb = instance.web.qweb;
8 instance.web.views.add('list', 'instance.web.ListView');
9 instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ {
10     _template: 'ListView',
11     display_name: _lt('List'),
12     defaults: {
13         // records can be selected one by one
14         'selectable': true,
15         // list rows can be deleted
16         'deletable': false,
17         // whether the column headers should be displayed
18         'header': true,
19         // display addition button, with that label
20         'addable': _lt("Create"),
21         // whether the list view can be sorted, note that once a view has been
22         // sorted it can not be reordered anymore
23         'sortable': true,
24         // whether the view rows can be reordered (via vertical drag & drop)
25         'reorderable': true,
26         'action_buttons': true,
27         //whether the editable property of the view has to be disabled
28         'disable_editable_mode': false,
29     },
30     view_type: 'tree',
31     events: {
32         'click thead th.oe_sortable[data-id]': 'sort_by_column'
33     },
34     /**
35      * Core class for list-type displays.
36      *
37      * As a view, needs a number of view-related parameters to be correctly
38      * instantiated, provides options and overridable methods for behavioral
39      * customization.
40      *
41      * See constructor parameters and method documentations for information on
42      * the default behaviors and possible options for the list view.
43      *
44      * @constructs instance.web.ListView
45      * @extends instance.web.View
46      *
47      * @param parent parent object
48      * @param {instance.web.DataSet} dataset the dataset the view should work with
49      * @param {String} view_id the listview's identifier, if any
50      * @param {Object} options A set of options used to configure the view
51      * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox)
52      * @param {Boolean} [options.header=true] should the list's header be displayed
53      * @param {Boolean} [options.deletable=true] are the list rows deletable
54      * @param {void|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button.
55      * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
56      * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
57      */
58     init: function(parent, dataset, view_id, options) {
59         var self = this;
60         this._super(parent);
61         this.set_default_options(_.extend({}, this.defaults, options || {}));
62         this.dataset = dataset;
63         this.model = dataset.model;
64         this.view_id = view_id;
65         this.previous_colspan = null;
66         this.colors = null;
67         this.fonts = null;
68
69         this.columns = [];
70
71         this.records = new Collection();
72
73         this.set_groups(new (this.options.GroupsType)(this));
74
75         if (this.dataset instanceof instance.web.DataSetStatic) {
76             this.groups.datagroup = new StaticDataGroup(this.dataset);
77         } else {
78             this.groups.datagroup = new DataGroup(
79                 this, this.model,
80                 dataset.get_domain(),
81                 dataset.get_context());
82             this.groups.datagroup.sort = this.dataset._sort;
83         }
84
85         this.page = 0;
86         this.records.bind('change', function (event, record, key) {
87             if (!_(self.aggregate_columns).chain()
88                     .pluck('name').contains(key).value()) {
89                 return;
90             }
91             self.compute_aggregates();
92         });
93
94         this.no_leaf = false;
95         this.grouped = false;
96     },
97     view_loading: function(r) {
98         return this.load_list(r);
99     },
100     set_default_options: function (options) {
101         this._super(options);
102         _.defaults(this.options, {
103             GroupsType: instance.web.ListView.Groups,
104             ListType: instance.web.ListView.List
105         });
106     },
107
108     /**
109      * Retrieves the view's number of records per page (|| section)
110      *
111      * options > defaults > parent.action.limit > indefinite
112      *
113      * @returns {Number|null}
114      */
115     limit: function () {
116         if (this._limit === undefined) {
117             this._limit = (this.options.limit
118                         || this.defaults.limit
119                         || (this.getParent().action || {}).limit
120                         || 80);
121         }
122         return this._limit;
123     },
124     /**
125      * Set a custom Group construct as the root of the List View.
126      *
127      * @param {instance.web.ListView.Groups} groups
128      */
129     set_groups: function (groups) {
130         var self = this;
131         if (this.groups) {
132             $(this.groups).unbind("selected deleted action row_link");
133             delete this.groups;
134         }
135
136         this.groups = groups;
137         $(this.groups).bind({
138             'selected': function (e, ids, records, deselected) {
139                 self.do_select(ids, records, deselected);
140             },
141             'deleted': function (e, ids) {
142                 self.do_delete(ids);
143             },
144             'action': function (e, action_name, id, callback) {
145                 self.do_button_action(action_name, id, callback);
146             },
147             'row_link': function (e, id, dataset, view) {
148                 self.do_activate_record(dataset.index, id, dataset, view);
149             }
150         });
151     },
152     /**
153      * View startup method, the default behavior is to set the ``oe_list``
154      * class on its root element and to perform an RPC load call.
155      *
156      * @returns {$.Deferred} loading promise
157      */
158     start: function() {
159         this.$el.addClass('oe_list');
160         return this._super();
161     },
162     /**
163      * Returns the style for the provided record in the current view (from the
164      * ``@colors`` and ``@fonts`` attributes)
165      *
166      * @param {Record} record record for the current row
167      * @returns {String} CSS style declaration
168      */
169     style_for: function (record) {
170         var style= '';
171
172         var context = _.extend({}, record.attributes, {
173             uid: this.session.uid,
174             current_date: new Date().toString('yyyy-MM-dd')
175             // TODO: time, datetime, relativedelta
176         });
177         var i;
178         var pair;
179         var expression;
180         if (this.fonts) {
181             for(i=0, len=this.fonts.length; i<len; ++i) {
182                 pair = this.fonts[i];
183                 var font = pair[0];
184                 expression = pair[1];
185                 if (py.evaluate(expression, context).toJSON()) {
186                     switch(font) {
187                     case 'bold':
188                         style += 'font-weight: bold;';
189                         break;
190                     case 'italic':
191                         style += 'font-style: italic;';
192                         break;
193                     case 'underline':
194                         style += 'text-decoration: underline;';
195                         break;
196                     }
197                 }
198             }
199         }
200
201         if (!this.colors) { return style; }
202         for(i=0, len=this.colors.length; i<len; ++i) {
203             pair = this.colors[i];
204             var color = pair[0];
205             expression = pair[1];
206             if (py.evaluate(expression, context).toJSON()) {
207                 return style += 'color: ' + color + ';';
208             }
209             // TODO: handle evaluation errors
210         }
211         return style;
212     },
213     /**
214      * Called after loading the list view's description, sets up such things
215      * as the view table's columns, renders the table itself and hooks up the
216      * various table-level and row-level DOM events (action buttons, deletion
217      * buttons, selection of records, [New] button, selection of a given
218      * record, ...)
219      *
220      * Sets up the following:
221      *
222      * * Processes arch and fields to generate a complete field descriptor for each field
223      * * Create the table itself and allocate visible columns
224      * * Hook in the top-level (header) [New|Add] and [Delete] button
225      * * Sets up showing/hiding the top-level [Delete] button based on records being selected or not
226      * * Sets up event handlers for action buttons and per-row deletion button
227      * * Hooks global callback for clicking on a row
228      * * Sets up its sidebar, if any
229      *
230      * @param {Object} data wrapped fields_view_get result
231      * @param {Object} data.fields_view fields_view_get result (processed)
232      * @param {Object} data.fields_view.fields mapping of fields for the current model
233      * @param {Object} data.fields_view.arch current list view descriptor
234      * @param {Boolean} grouped Is the list view grouped
235      */
236     load_list: function(data) {
237         var self = this;
238         this.fields_view = data;
239         this.name = "" + this.fields_view.arch.attrs.string;
240
241         if (this.fields_view.arch.attrs.colors) {
242             this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain()
243                 .compact()
244                 .map(function(color_pair) {
245                     var pair = color_pair.split(':'),
246                         color = pair[0],
247                         expr = pair[1];
248                     return [color, py.parse(py.tokenize(expr)), expr];
249                 }).value();
250         }
251
252         if (this.fields_view.arch.attrs.fonts) {
253             this.fonts = _(this.fields_view.arch.attrs.fonts.split(';')).chain().compact()
254                 .map(function(font_pair) {
255                     var pair = font_pair.split(':'),
256                         font = pair[0],
257                         expr = pair[1];
258                     return [font, py.parse(py.tokenize(expr)), expr];
259                 }).value();
260         }
261
262         this.setup_columns(this.fields_view.fields, this.grouped);
263
264         this.$el.html(QWeb.render(this._template, this));
265         this.$el.addClass(this.fields_view.arch.attrs['class']);
266
267         // Head hook
268         // Selecting records
269         this.$el.find('.oe_list_record_selector').click(function(){
270             self.$el.find('.oe_list_record_selector input').prop('checked',
271                 self.$el.find('.oe_list_record_selector').prop('checked')  || false);
272             var selection = self.groups.get_selection();
273             $(self.groups).trigger(
274                 'selected', [selection.ids, selection.records]);
275         });
276
277         // Add button
278         if (!this.$buttons) {
279             this.$buttons = $(QWeb.render("ListView.buttons", {'widget':self}));
280             if (this.options.$buttons) {
281                 this.$buttons.appendTo(this.options.$buttons);
282             } else {
283                 this.$el.find('.oe_list_buttons').replaceWith(this.$buttons);
284             }
285             this.$buttons.find('.oe_list_add')
286                     .click(this.proxy('do_add_record'))
287                     .prop('disabled', this.grouped);
288         }
289
290         // Pager
291         if (!this.$pager) {
292             this.$pager = $(QWeb.render("ListView.pager", {'widget':self}));
293             if (this.options.$buttons) {
294                 this.$pager.appendTo(this.options.$pager);
295             } else {
296                 this.$el.find('.oe_list_pager').replaceWith(this.$pager);
297             }
298
299             this.$pager
300                 .on('click', 'a[data-pager-action]', function () {
301                     var $this = $(this);
302                     var max_page = Math.floor(self.dataset.size() / self.limit());
303                     switch ($this.data('pager-action')) {
304                         case 'first':
305                             self.page = 0; break;
306                         case 'last':
307                             self.page = max_page - 1;
308                             break;
309                         case 'next':
310                             self.page += 1; break;
311                         case 'previous':
312                             self.page -= 1; break;
313                     }
314                     if (self.page < 0) {
315                         self.page = max_page;
316                     } else if (self.page > max_page) {
317                         self.page = 0;
318                     }
319                     self.reload_content();
320                 }).find('.oe_list_pager_state')
321                     .click(function (e) {
322                         e.stopPropagation();
323                         var $this = $(this);
324
325                         var $select = $('<select>')
326                             .appendTo($this.empty())
327                             .click(function (e) {e.stopPropagation();})
328                             .append('<option value="80">80</option>' +
329                                     '<option value="200">200</option>' +
330                                     '<option value="500">500</option>' +
331                                     '<option value="2000">2000</option>' +
332                                     '<option value="NaN">' + _t("Unlimited") + '</option>')
333                             .change(function () {
334                                 var val = parseInt($select.val(), 10);
335                                 self._limit = (isNaN(val) ? null : val);
336                                 self.page = 0;
337                                 self.reload_content();
338                             }).blur(function() {
339                                 $(this).trigger('change');
340                             })
341                             .val(self._limit || 'NaN');
342                     });
343         }
344
345         // Sidebar
346         if (!this.sidebar && this.options.$sidebar) {
347             this.sidebar = new instance.web.Sidebar(this);
348             this.sidebar.appendTo(this.options.$sidebar);
349             this.sidebar.add_items('other', _.compact([
350                 { label: _t("Export"), callback: this.on_sidebar_export },
351                 self.is_action_enabled('delete') && { label: _t('Delete'), callback: this.do_delete_selected }
352             ]));
353             this.sidebar.add_toolbar(this.fields_view.toolbar);
354             this.sidebar.$el.hide();
355         }
356         //Sort
357         if(this.dataset._sort.length){
358             if(this.dataset._sort[0].indexOf('-') == -1){
359                 this.$el.find('th[data-id=' + this.dataset._sort[0] + ']').addClass("sortdown");
360             }else {
361                 this.$el.find('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("sortup");
362             }
363         }
364         this.trigger('list_view_loaded', data, this.grouped);
365     },
366     sort_by_column: function (e) {
367         e.stopPropagation();
368         var $column = $(e.currentTarget);
369         var col_name = $column.data('id');
370         var field = this.fields_view.fields[col_name];
371         // test if the field is a function field with store=false, since it's impossible
372         // for the server to sort those fields we desactivate the feature
373         if (field && field.store === false) {
374             return false;
375         }
376         this.dataset.sort(col_name);
377         if($column.hasClass("sortdown") || $column.hasClass("sortup"))  {
378             $column.toggleClass("sortup sortdown");
379         } else {
380             $column.addClass("sortdown");
381         }
382         $column.siblings('.oe_sortable').removeClass("sortup sortdown");
383
384         this.reload_content();
385     },
386     /**
387      * Configures the ListView pager based on the provided dataset's information
388      *
389      * Horrifying side-effect: sets the dataset's data on this.dataset?
390      *
391      * @param {instance.web.DataSet} dataset
392      */
393     configure_pager: function (dataset) {
394         this.dataset.ids = dataset.ids;
395         // Not exactly clean
396         if (dataset._length) {
397             this.dataset._length = dataset._length;
398         }
399
400         var total = dataset.size();
401         var limit = this.limit() || total;
402         if (total === 0)
403             this.$pager.hide();
404         else
405             this.$pager.css("display", "");
406         this.$pager.toggleClass('oe_list_pager_single_page', (total <= limit));
407         var spager = '-';
408         if (total) {
409             var range_start = this.page * limit + 1;
410             var range_stop = range_start - 1 + limit;
411             if (this.records.length) {
412                 range_stop = range_start - 1 + this.records.length;
413             }
414             if (range_stop > total) {
415                 range_stop = total;
416             }
417             spager = _.str.sprintf(_t("%d-%d of %d"), range_start, range_stop, total);
418         }
419
420         this.$pager.find('.oe_list_pager_state').text(spager);
421     },
422     /**
423      * Sets up the listview's columns: merges view and fields data, move
424      * grouped-by columns to the front of the columns list and make them all
425      * visible.
426      *
427      * @param {Object} fields fields_view_get's fields section
428      * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
429      */
430     setup_columns: function (fields, grouped) {
431         var registry = instance.web.list.columns;
432         this.columns.splice(0, this.columns.length);
433         this.columns.push.apply(this.columns,
434             _(this.fields_view.arch.children).map(function (field) {
435                 var id = field.attrs.name;
436                 return registry.for_(id, fields[id], field);
437         }));
438         if (grouped) {
439             this.columns.unshift(
440                 new instance.web.list.MetaColumn('_group', _t("Group")));
441         }
442
443         this.visible_columns = _.filter(this.columns, function (column) {
444             return column.invisible !== '1';
445         });
446
447         this.aggregate_columns = _(this.visible_columns).invoke('to_aggregate');
448     },
449     /**
450      * Used to handle a click on a table row, if no other handler caught the
451      * event.
452      *
453      * The default implementation asks the list view's view manager to switch
454      * to a different view (by calling
455      * :js:func:`~instance.web.ViewManager.on_mode_switch`), using the
456      * provided record index (within the current list view's dataset).
457      *
458      * If the index is null, ``switch_to_record`` asks for the creation of a
459      * new record.
460      *
461      * @param {Number|void} index the record index (in the current dataset) to switch to
462      * @param {String} [view="page"] the view type to switch to
463      */
464     select_record:function (index, view) {
465         view = view || index === null || index === undefined ? 'form' : 'form';
466         this.dataset.index = index;
467         _.delay(_.bind(function () {
468             this.do_switch_view(view);
469         }, this));
470     },
471     do_show: function () {
472         this._super();
473         if (this.$buttons) {
474             this.$buttons.show();
475         }
476         if (this.$pager) {
477             this.$pager.show();
478         }
479     },
480     do_hide: function () {
481         if (this.sidebar) {
482             this.sidebar.$el.hide();
483         }
484         if (this.$buttons) {
485             this.$buttons.hide();
486         }
487         if (this.$pager) {
488             this.$pager.hide();
489         }
490         this._super();
491     },
492     /**
493      * Reloads the list view based on the current settings (dataset & al)
494      *
495      * @deprecated
496      * @param {Boolean} [grouped] Should the list be displayed grouped
497      * @param {Object} [context] context to send the server while loading the view
498      */
499     reload_view: function (grouped, context, initial) {
500         return this.load_view(context);
501     },
502     /**
503      * re-renders the content of the list view
504      *
505      * @returns {$.Deferred} promise to content reloading
506      */
507     reload_content: synchronized(function () {
508         var self = this;
509         self.$el.find('.oe_list_record_selector').prop('checked', false);
510         this.records.reset();
511         var reloaded = $.Deferred();
512         this.$el.find('.oe_list_content').append(
513             this.groups.render(function () {
514                 if (self.dataset.index == null) {
515                     if (self.records.length) {
516                         self.dataset.index = 0;
517                     }
518                 } else if (self.dataset.index >= self.records.length) {
519                     self.dataset.index = 0;
520                 }
521
522                 self.compute_aggregates();
523                 reloaded.resolve();
524             }));
525         this.do_push_state({
526             page: this.page,
527             limit: this._limit
528         });
529         return reloaded.promise();
530     }),
531     reload: function () {
532         return this.reload_content();
533     },
534     reload_record: function (record) {
535         var self = this;
536         return this.dataset.read_ids(
537             [record.get('id')],
538             _.pluck(_(this.columns).filter(function (r) {
539                     return r.tag === 'field';
540                 }), 'name'),
541             {check_access_rule: true}
542         ).done(function (records) {
543             var values = records[0];
544             if (!values) {
545                 self.records.remove(record);
546                 return;
547             }
548             _(_.keys(values)).each(function(key){
549                 record.set(key, values[key], {silent: true});
550             });
551             record.trigger('change', record);
552         });
553     },
554
555     do_load_state: function(state, warm) {
556         var reload = false;
557         if (state.page && this.page !== state.page) {
558             this.page = state.page;
559             reload = true;
560         }
561         if (state.limit) {
562             if (_.isString(state.limit)) {
563                 state.limit = null;
564             }
565             if (state.limit !== this._limit) {
566                 this._limit = state.limit;
567                 reload = true;
568             }
569         }
570         if (reload) {
571             this.reload_content();
572         }
573     },
574     /**
575      * Handler for the result of eval_domain_and_context, actually perform the
576      * searching
577      *
578      * @param {Object} results results of evaluating domain and process for a search
579      */
580     do_search: function (domain, context, group_by) {
581         this.page = 0;
582         this.groups.datagroup = new DataGroup(
583             this, this.model, domain, context, group_by);
584         this.groups.datagroup.sort = this.dataset._sort;
585
586         if (_.isEmpty(group_by) && !context['group_by_no_leaf']) {
587             group_by = null;
588         }
589         this.no_leaf = !!context['group_by_no_leaf'];
590         this.grouped = !!group_by;
591
592         return this.alive(this.load_view(context)).then(
593             this.proxy('reload_content'));
594     },
595     /**
596      * Handles the signal to delete lines from the records list
597      *
598      * @param {Array} ids the ids of the records to delete
599      */
600     do_delete: function (ids) {
601         if (!(ids.length && confirm(_t("Do you really want to remove these records?")))) {
602             return;
603         }
604         var self = this;
605         return $.when(this.dataset.unlink(ids)).done(function () {
606             _(ids).each(function (id) {
607                 self.records.remove(self.records.get(id));
608             });
609             if (self.records.length === 0 && self.dataset.size() > 0) {
610                 //Trigger previous manually to navigate to previous page, 
611                 //If all records are deleted on current page.
612                 self.$pager.find('ul li:first a').trigger('click');
613             } else if (self.dataset.size() == self.limit()) {
614                 //Reload listview to update current page with next page records 
615                 //because pager going to be hidden if dataset.size == limit
616                 self.reload();
617             } else {
618                 self.configure_pager(self.dataset);
619             }
620             self.compute_aggregates();
621         });
622     },
623     /**
624      * Handles the signal indicating that a new record has been selected
625      *
626      * @param {Array} ids selected record ids
627      * @param {Array} records selected record values
628      */
629     do_select: function (ids, records, deselected) {
630         // uncheck header hook if at least one row has been deselected
631         if (deselected) {
632             this.$('.oe_list_record_selector').prop('checked', false);
633         }
634
635         if (!ids.length) {
636             this.dataset.index = 0;
637             if (this.sidebar) {
638                 this.sidebar.$el.hide();
639             }
640             this.compute_aggregates();
641             return;
642         }
643
644         this.dataset.index = _(this.dataset.ids).indexOf(ids[0]);
645         if (this.sidebar) {
646             this.options.$sidebar.show();
647             this.sidebar.$el.show();
648         }
649
650         this.compute_aggregates(_(records).map(function (record) {
651             return {count: 1, values: record};
652         }));
653     },
654     /**
655      * Handles action button signals on a record
656      *
657      * @param {String} name action name
658      * @param {Object} id id of the record the action should be called on
659      * @param {Function} callback should be called after the action is executed, if non-null
660      */
661     do_button_action: function (name, id, callback) {
662         this.handle_button(name, id, callback);
663     },
664     /**
665      * Base handling of buttons, can be called when overriding do_button_action
666      * in order to bypass parent overrides.
667      *
668      * The callback will be provided with the ``id`` as its parameter, in case
669      * handle_button's caller had to alter the ``id`` (or even create one)
670      * while not being ``callback``'s creator.
671      *
672      * This method should not be overridden.
673      *
674      * @param {String} name action name
675      * @param {Object} id id of the record the action should be called on
676      * @param {Function} callback should be called after the action is executed, if non-null
677      */
678     handle_button: function (name, id, callback) {
679         var action = _.detect(this.columns, function (field) {
680             return field.name === name;
681         });
682         if (!action) { return; }
683         if ('confirm' in action && !window.confirm(action.confirm)) {
684             return;
685         }
686
687         var c = new instance.web.CompoundContext();
688         c.set_eval_context(_.extend({
689             active_id: id,
690             active_ids: [id],
691             active_model: this.dataset.model
692         }, this.records.get(id).toContext()));
693         if (action.context) {
694             c.add(action.context);
695         }
696         action.context = c;
697         this.do_execute_action(
698             action, this.dataset, id, _.bind(callback, null, id));
699     },
700     /**
701      * Handles the activation of a record (clicking on it)
702      *
703      * @param {Number} index index of the record in the dataset
704      * @param {Object} id identifier of the activated record
705      * @param {instance.web.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups)
706      */
707     do_activate_record: function (index, id, dataset, view) {
708         this.dataset.ids = dataset.ids;
709         this.select_record(index, view);
710     },
711     /**
712      * Handles signal for the addition of a new record (can be a creation,
713      * can be the addition from a remote source, ...)
714      *
715      * The default implementation is to switch to a new record on the form view
716      */
717     do_add_record: function () {
718         this.select_record(null);
719     },
720     /**
721      * Handles deletion of all selected lines
722      */
723     do_delete_selected: function () {
724         var ids = this.groups.get_selection().ids;
725         if (ids.length) {
726             this.do_delete(this.groups.get_selection().ids);
727         } else {
728             this.do_warn(_t("Warning"), _t("You must select at least one record."));
729         }
730     },
731     /**
732      * Computes the aggregates for the current list view, either on the
733      * records provided or on the records of the internal
734      * :js:class:`~instance.web.ListView.Group`, by calling
735      * :js:func:`~instance.web.ListView.group.get_records`.
736      *
737      * Then displays the aggregates in the table through
738      * :js:method:`~instance.web.ListView.display_aggregates`.
739      *
740      * @param {Array} [records]
741      */
742     compute_aggregates: function (records) {
743         var columns = _(this.aggregate_columns).filter(function (column) {
744             return column['function']; });
745         if (_.isEmpty(columns)) { return; }
746
747         if (_.isEmpty(records)) {
748             records = this.groups.get_records();
749         }
750         records = _(records).compact();
751
752         var count = 0, sums = {};
753         _(columns).each(function (column) {
754             switch (column['function']) {
755                 case 'max':
756                     sums[column.id] = -Infinity;
757                     break;
758                 case 'min':
759                     sums[column.id] = Infinity;
760                     break;
761                 default:
762                     sums[column.id] = 0;
763             }
764         });
765         _(records).each(function (record) {
766             count += record.count || 1;
767             _(columns).each(function (column) {
768                 var field = column.id,
769                     value = record.values[field];
770                 switch (column['function']) {
771                     case 'sum':
772                         sums[field] += value;
773                         break;
774                     case 'avg':
775                         sums[field] += record.count * value;
776                         break;
777                     case 'min':
778                         if (sums[field] > value) {
779                             sums[field] = value;
780                         }
781                         break;
782                     case 'max':
783                         if (sums[field] < value) {
784                             sums[field] = value;
785                         }
786                         break;
787                 }
788             });
789         });
790
791         var aggregates = {};
792         _(columns).each(function (column) {
793             var field = column.id;
794             switch (column['function']) {
795                 case 'avg':
796                     aggregates[field] = {value: sums[field] / count};
797                     break;
798                 default:
799                     aggregates[field] = {value: sums[field]};
800             }
801         });
802
803         this.display_aggregates(aggregates);
804     },
805     display_aggregates: function (aggregation) {
806         var self = this;
807         var $footer_cells = this.$el.find('.oe_list_footer');
808         _(this.aggregate_columns).each(function (column) {
809             if (!column['function']) {
810                 return;
811             }
812
813             $footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id))
814                 .html(column.format(aggregation, { process_modifiers: false }));
815         });
816     },
817     get_selected_ids: function() {
818         var ids = this.groups.get_selection().ids;
819         return ids;
820     },
821     /**
822      * Calculate the active domain of the list view. This should be done only
823      * if the header checkbox has been checked. This is done by evaluating the
824      * search results, and then adding the dataset domain (i.e. action domain).
825      */
826     get_active_domain: function () {
827         var self = this;
828         if (this.$('.oe_list_record_selector').prop('checked')) {
829             var search_view = this.getParent().searchview;
830             var search_data = search_view.build_search_data();
831             return instance.web.pyeval.eval_domains_and_contexts({
832                 domains: search_data.domains,
833                 contexts: search_data.contexts,
834                 group_by_seq: search_data.groupbys || []
835             }).then(function (results) {
836                 var domain = self.dataset.domain.concat(results.domain || []);
837                 return domain
838             });
839         }
840         else {
841             return $.Deferred().resolve();
842         }
843     },
844     /**
845      * Adds padding columns at the start or end of all table rows (including
846      * field names row)
847      *
848      * @param {Number} count number of columns to add
849      * @param {Object} options
850      * @param {"before"|"after"} [options.position="after"] insertion position for the new columns
851      * @param {Object} [options.except] content row to not pad
852      */
853     pad_columns: function (count, options) {
854         options = options || {};
855         // padding for action/pager header
856         var $first_header = this.$el.find('thead tr:first th');
857         var colspan = $first_header.attr('colspan');
858         if (colspan) {
859             if (!this.previous_colspan) {
860                 this.previous_colspan = colspan;
861             }
862             $first_header.attr('colspan', parseInt(colspan, 10) + count);
863         }
864         // Padding for column titles, footer and data rows
865         var $rows = this.$el
866                 .find('.oe_list_header_columns, tr:not(thead tr)')
867                 .not(options['except']);
868         var newcols = new Array(count+1).join('<td class="oe_list_padding"></td>');
869         if (options.position === 'before') {
870             $rows.prepend(newcols);
871         } else {
872             $rows.append(newcols);
873         }
874     },
875     /**
876      * Removes all padding columns of the table
877      */
878     unpad_columns: function () {
879         this.$el.find('.oe_list_padding').remove();
880         if (this.previous_colspan) {
881             this.$el
882                     .find('thead tr:first th')
883                     .attr('colspan', this.previous_colspan);
884             this.previous_colspan = null;
885         }
886     },
887     no_result: function () {
888         this.$el.find('.oe_view_nocontent').remove();
889         if (this.groups.group_by
890             || !this.options.action
891             || !this.options.action.help) {
892             return;
893         }
894         this.$el.find('table:first').hide();
895         this.$el.prepend(
896             $('<div class="oe_view_nocontent">').html(this.options.action.help)
897         );
898         var create_nocontent = this.$buttons;
899         this.$el.find('.oe_view_nocontent').click(function() {
900             create_nocontent.openerpBounce();
901         });
902     }
903 });
904 instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.ListView.List# */{
905     /**
906      * List display for the ListView, handles basic DOM events and transforms
907      * them in the relevant higher-level events, to which the list view (or
908      * other consumers) can subscribe.
909      *
910      * Events on this object are registered via jQuery.
911      *
912      * Available events:
913      *
914      * `selected`
915      *   Triggered when a row is selected (using check boxes), provides an
916      *   array of ids of all the selected records.
917      * `deleted`
918      *   Triggered when deletion buttons are hit, provide an array of ids of
919      *   all the records being marked for suppression.
920      * `action`
921      *   Triggered when an action button is clicked, provides two parameters:
922      *
923      *   * The name of the action to execute (as a string)
924      *   * The id of the record to execute the action on
925      * `row_link`
926      *   Triggered when a row of the table is clicked, provides the index (in
927      *   the rows array) and id of the selected record to the handle function.
928      *
929      * @constructs instance.web.ListView.List
930      * @extends instance.web.Class
931      * 
932      * @param {Object} opts display options, identical to those of :js:class:`instance.web.ListView`
933      */
934     init: function (group, opts) {
935         var self = this;
936         this.group = group;
937         this.view = group.view;
938         this.session = this.view.session;
939
940         this.options = opts.options;
941         this.columns = opts.columns;
942         this.dataset = opts.dataset;
943         this.records = opts.records;
944
945         this.record_callbacks = {
946             'remove': function (event, record) {
947                 var id = record.get('id');
948                 self.dataset.remove_ids([id]);
949                 var $row = self.$current.children('[data-id=' + id + ']');
950                 var index = $row.data('index');
951                 $row.remove();
952             },
953             'reset': function () { return self.on_records_reset(); },
954             'change': function (event, record, attribute, value, old_value) {
955                 var $row;
956                 if (attribute === 'id') {
957                     if (old_value) {
958                         throw new Error(_.str.sprintf( _t("Setting 'id' attribute on existing record %s"),
959                             JSON.stringify(record.attributes) ));
960                     }
961                     self.dataset.add_ids([value], self.records.indexOf(record));
962                     // Set id on new record
963                     $row = self.$current.children('[data-id=false]');
964                 } else {
965                     $row = self.$current.children(
966                         '[data-id=' + record.get('id') + ']');
967                 }
968                 $row.replaceWith(self.render_record(record));
969             },
970             'add': function (ev, records, record, index) {
971                 var $new_row = $(self.render_record(record));
972                 var id = record.get('id');
973                 if (id) { self.dataset.add_ids([id], index); }
974
975                 if (index === 0) {
976                     $new_row.prependTo(self.$current);
977                 } else {
978                     var previous_record = records.at(index-1),
979                         $previous_sibling = self.$current.children(
980                                 '[data-id=' + previous_record.get('id') + ']');
981                     $new_row.insertAfter($previous_sibling);
982                 }
983             }
984         };
985         _(this.record_callbacks).each(function (callback, event) {
986             this.records.bind(event, callback);
987         }, this);
988
989         this.$current = $('<tbody>')
990             .delegate('input[readonly=readonly]', 'click', function (e) {
991                 /*
992                     Against all logic and sense, as of right now @readonly
993                     apparently does nothing on checkbox and radio inputs, so
994                     the trick of using @readonly to have, well, readonly
995                     checkboxes (which still let clicks go through) does not
996                     work out of the box. We *still* need to preventDefault()
997                     on the event, otherwise the checkbox's state *will* toggle
998                     on click
999                  */
1000                 e.preventDefault();
1001             })
1002             .delegate('th.oe_list_record_selector', 'click', function (e) {
1003                 e.stopPropagation();
1004                 var selection = self.get_selection();
1005                 var checked = $(e.currentTarget).find('input').prop('checked');
1006                 $(self).trigger(
1007                         'selected', [selection.ids, selection.records, ! checked]);
1008             })
1009             .delegate('td.oe_list_record_delete button', 'click', function (e) {
1010                 e.stopPropagation();
1011                 var $row = $(e.target).closest('tr');
1012                 $(self).trigger('deleted', [[self.row_id($row)]]);
1013             })
1014             .delegate('td.oe_list_field_cell button', 'click', function (e) {
1015                 e.stopPropagation();
1016                 var $target = $(e.currentTarget),
1017                       field = $target.closest('td').data('field'),
1018                        $row = $target.closest('tr'),
1019                   record_id = self.row_id($row);
1020                 
1021                 if ($target.attr('disabled')) {
1022                     return;
1023                 }
1024                 $target.attr('disabled', 'disabled');
1025
1026                 // note: $.data converts data to number if it's composed only
1027                 // of digits, nice when storing actual numbers, not nice when
1028                 // storing strings composed only of digits. Force the action
1029                 // name to be a string
1030                 $(self).trigger('action', [field.toString(), record_id, function (id) {
1031                     $target.removeAttr('disabled');
1032                     return self.reload_record(self.records.get(id));
1033                 }]);
1034             })
1035             .delegate('a', 'click', function (e) {
1036                 e.stopPropagation();
1037             })
1038             .delegate('tr', 'click', function (e) {
1039                 var row_id = self.row_id(e.currentTarget);
1040                 if (row_id) {
1041                     e.stopPropagation();
1042                     if (!self.dataset.select_id(row_id)) {
1043                         throw new Error(_t("Could not find id in dataset"));
1044                     }
1045                     self.row_clicked(e);
1046                 }
1047             });
1048     },
1049     row_clicked: function (e, view) {
1050         $(this).trigger(
1051             'row_link',
1052             [this.dataset.ids[this.dataset.index],
1053              this.dataset, view]);
1054     },
1055     render_cell: function (record, column) {
1056         var value;
1057         if(column.type === 'reference') {
1058             value = record.get(column.id);
1059             var ref_match;
1060             // Ensure that value is in a reference "shape", otherwise we're
1061             // going to loop on performing name_get after we've resolved (and
1062             // set) a human-readable version. m2o does not have this issue
1063             // because the non-human-readable is just a number, where the
1064             // human-readable version is a pair
1065             if (value && (ref_match = /^([\w\.]+),(\d+)$/.exec(value))) {
1066                 // reference values are in the shape "$model,$id" (as a
1067                 // string), we need to split and name_get this pair in order
1068                 // to get a correctly displayable value in the field
1069                 var model = ref_match[1],
1070                     id = parseInt(ref_match[2], 10);
1071                 new instance.web.DataSet(this.view, model).name_get([id]).done(function(names) {
1072                     if (!names.length) { return; }
1073                     record.set(column.id + '__display', names[0][1]);
1074                 });
1075             }
1076         } else if (column.type === 'many2one') {
1077             value = record.get(column.id);
1078             // m2o values are usually name_get formatted, [Number, String]
1079             // pairs, but in some cases only the id is provided. In these
1080             // cases, we need to perform a name_get call to fetch the actual
1081             // displayable value
1082             if (typeof value === 'number' || value instanceof Number) {
1083                 // fetch the name, set it on the record (in the right field)
1084                 // and let the various registered events handle refreshing the
1085                 // row
1086                 new instance.web.DataSet(this.view, column.relation)
1087                         .name_get([value]).done(function (names) {
1088                     if (!names.length) { return; }
1089                     record.set(column.id, names[0]);
1090                 });
1091             }
1092         } else if (column.type === 'many2many') {
1093             value = record.get(column.id);
1094             // non-resolved (string) m2m values are arrays
1095             if (value instanceof Array && !_.isEmpty(value)
1096                     && !record.get(column.id + '__display')) {
1097                 var ids;
1098                 // they come in two shapes:
1099                 if (value[0] instanceof Array) {
1100                     var command = value[0];
1101                     // 1. an array of m2m commands (usually (6, false, ids))
1102                     if (command[0] !== 6) {
1103                         throw new Error(_.str.sprintf( _t("Unknown m2m command %s"), command[0]));
1104                     }
1105                     ids = command[2];
1106                 } else {
1107                     // 2. an array of ids
1108                     ids = value;
1109                 }
1110                 new instance.web.Model(column.relation)
1111                     .call('name_get', [ids]).done(function (names) {
1112                         // FIXME: nth horrible hack in this poor listview
1113                         record.set(column.id + '__display',
1114                                    _(names).pluck(1).join(', '));
1115                         record.set(column.id, ids);
1116                     });
1117                 // temp empty value
1118                 record.set(column.id, false);
1119             }
1120         }
1121         return column.format(record.toForm().data, {
1122             model: this.dataset.model,
1123             id: record.get('id')
1124         });
1125     },
1126     render: function () {
1127         this.$current.empty().append(
1128             QWeb.render('ListView.rows', _.extend({
1129                     render_cell: function () {
1130                         return self.render_cell.apply(self, arguments); }
1131                 }, this)));
1132         this.pad_table_to(4);
1133     },
1134     pad_table_to: function (count) {
1135         if (this.records.length >= count ||
1136                 _(this.columns).any(function(column) { return column.meta; })) {
1137             return;
1138         }
1139         var cells = [];
1140         if (this.options.selectable) {
1141             cells.push('<th class="oe_list_record_selector"></td>');
1142         }
1143         _(this.columns).each(function(column) {
1144             if (column.invisible === '1') {
1145                 return;
1146             }
1147             cells.push('<td title="' + column.string + '">&nbsp;</td>');
1148         });
1149         if (this.options.deletable) {
1150             cells.push('<td class="oe_list_record_delete"><button type="button" style="visibility: hidden"> </button></td>');
1151         }
1152         cells.unshift('<tr>');
1153         cells.push('</tr>');
1154
1155         var row = cells.join('');
1156         this.$current
1157             .children('tr:not([data-id])').remove().end()
1158             .append(new Array(count - this.records.length + 1).join(row));
1159     },
1160     /**
1161      * Gets the ids of all currently selected records, if any
1162      * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves.
1163      */
1164     get_selection: function () {
1165         var result = {ids: [], records: []};
1166         if (!this.options.selectable) {
1167             return result;
1168         }
1169         var records = this.records;
1170         this.$current.find('th.oe_list_record_selector input:checked')
1171                 .closest('tr').each(function () {
1172             var record = records.get($(this).data('id'));
1173             result.ids.push(record.get('id'));
1174             result.records.push(record.attributes);
1175         });
1176         return result;
1177     },
1178     /**
1179      * Returns the identifier of the object displayed in the provided table
1180      * row
1181      *
1182      * @param {Object} row the selected table row
1183      * @returns {Number|String} the identifier of the row's object
1184      */
1185     row_id: function (row) {
1186         return $(row).data('id');
1187     },
1188     /**
1189      * Death signal, cleans up list display
1190      */
1191     on_records_reset: function () {
1192         _(this.record_callbacks).each(function (callback, event) {
1193             this.records.unbind(event, callback);
1194         }, this);
1195         if (!this.$current) { return; }
1196         this.$current.remove();
1197     },
1198     get_records: function () {
1199         return this.records.map(function (record) {
1200             return {count: 1, values: record.attributes};
1201         });
1202     },
1203     /**
1204      * Reloads the provided record by re-reading its content from the server.
1205      *
1206      * @param {Record} record
1207      * @returns {$.Deferred} promise to the finalization of the reloading
1208      */
1209     reload_record: function (record) {
1210         return this.view.reload_record(record);
1211     },
1212     /**
1213      * Renders a list record to HTML
1214      *
1215      * @param {Record} record index of the record to render in ``this.rows``
1216      * @returns {String} QWeb rendering of the selected record
1217      */
1218     render_record: function (record) {
1219         var self = this;
1220         var index = this.records.indexOf(record);
1221         return QWeb.render('ListView.row', {
1222             columns: this.columns,
1223             options: this.options,
1224             record: record,
1225             row_parity: (index % 2 === 0) ? 'even' : 'odd',
1226             view: this.view,
1227             render_cell: function () {
1228                 return self.render_cell.apply(self, arguments); }
1229         });
1230     }
1231 });
1232 instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.web.ListView.Groups# */{
1233     passthrough_events: 'action deleted row_link',
1234     /**
1235      * Grouped display for the ListView. Handles basic DOM events and interacts
1236      * with the :js:class:`~DataGroup` bound to it.
1237      *
1238      * Provides events similar to those of
1239      * :js:class:`~instance.web.ListView.List`
1240      *
1241      * @constructs instance.web.ListView.Groups
1242      * @extends instance.web.Class
1243      *
1244      * @param {instance.web.ListView} view
1245      * @param {Object} [options]
1246      * @param {Collection} [options.records]
1247      * @param {Object} [options.options]
1248      * @param {Array} [options.columns]
1249      */
1250     init: function (view, options) {
1251         options = options || {};
1252         this.view = view;
1253         this.records = options.records || view.records;
1254         this.options = options.options || view.options;
1255         this.columns = options.columns || view.columns;
1256         this.datagroup = null;
1257
1258         this.$row = null;
1259         this.children = {};
1260
1261         this.page = 0;
1262
1263         var self = this;
1264         this.records.bind('reset', function () {
1265             return self.on_records_reset(); });
1266     },
1267     make_fragment: function () {
1268         return document.createDocumentFragment();
1269     },
1270     /**
1271      * Returns a DOM node after which a new tbody can be inserted, so that it
1272      * follows the provided row.
1273      *
1274      * Necessary to insert the result of a new group or list view within an
1275      * existing groups render, without losing track of the groups's own
1276      * elements
1277      *
1278      * @param {HTMLTableRowElement} row the row after which the caller wants to insert a body
1279      * @returns {HTMLTableSectionElement} element after which a tbody can be inserted
1280      */
1281     point_insertion: function (row) {
1282         var $row = $(row);
1283         var red_letter_tboday = $row.closest('tbody')[0];
1284
1285         var $next_siblings = $row.nextAll();
1286         if ($next_siblings.length) {
1287             var $root_kanal = $('<tbody>').insertAfter(red_letter_tboday);
1288
1289             $root_kanal.append($next_siblings);
1290             this.elements.splice(
1291                 _.indexOf(this.elements, red_letter_tboday),
1292                 0,
1293                 $root_kanal[0]);
1294         }
1295         return red_letter_tboday;
1296     },
1297     make_paginator: function () {
1298         var self = this;
1299         var $prev = $('<button type="button" data-pager-action="previous">&lt;</button>')
1300             .click(function (e) {
1301                 e.stopPropagation();
1302                 self.page -= 1;
1303
1304                 self.$row.closest('tbody').next()
1305                     .replaceWith(self.render());
1306             });
1307         var $next = $('<button type="button" data-pager-action="next">&gt;</button>')
1308             .click(function (e) {
1309                 e.stopPropagation();
1310                 self.page += 1;
1311
1312                 self.$row.closest('tbody').next()
1313                     .replaceWith(self.render());
1314             });
1315         this.$row.children().last()
1316             .addClass('oe_list_group_pagination')
1317             .append($prev)
1318             .append('<span class="oe_list_pager_state"></span>')
1319             .append($next);
1320     },
1321     open: function (point_insertion) {
1322         this.render().insertAfter(point_insertion);
1323
1324         var no_subgroups = _(this.datagroup.group_by).isEmpty(),
1325             records_terminated = !this.datagroup.context['group_by_no_leaf'];
1326         if (no_subgroups && records_terminated) {
1327             this.make_paginator();
1328         }
1329     },
1330     close: function () {
1331         this.$row.children().last().find('button').remove();
1332         this.$row.children().last().find('span').remove();
1333         this.records.reset();
1334     },
1335     /**
1336      * Prefixes ``$node`` with floated spaces in order to indent it relative
1337      * to its own left margin/baseline
1338      *
1339      * @param {jQuery} $node jQuery object to indent
1340      * @param {Number} level current nesting level, >= 1
1341      * @returns {jQuery} the indentation node created
1342      */
1343     indent: function ($node, level) {
1344         return $('<span>')
1345                 .css({'float': 'left', 'white-space': 'pre'})
1346                 .text(new Array(level).join('   '))
1347                 .prependTo($node);
1348     },
1349     render_groups: function (datagroups) {
1350         var self = this;
1351         var placeholder = this.make_fragment();
1352         _(datagroups).each(function (group) {
1353             if (self.children[group.value]) {
1354                 self.records.proxy(group.value).reset();
1355                 delete self.children[group.value];
1356             }
1357             var child = self.children[group.value] = new (self.view.options.GroupsType)(self.view, {
1358                 records: self.records.proxy(group.value),
1359                 options: self.options,
1360                 columns: self.columns
1361             });
1362             self.bind_child_events(child);
1363             child.datagroup = group;
1364
1365             var $row = child.$row = $('<tr class="oe_group_header">');
1366             if (group.openable && group.length) {
1367                 $row.click(function (e) {
1368                     if (!$row.data('open')) {
1369                         $row.data('open', true)
1370                             .find('span.ui-icon')
1371                                 .removeClass('ui-icon-triangle-1-e')
1372                                 .addClass('ui-icon-triangle-1-s');
1373                         child.open(self.point_insertion(e.currentTarget));
1374                     } else {
1375                         $row.removeData('open')
1376                             .find('span.ui-icon')
1377                                 .removeClass('ui-icon-triangle-1-s')
1378                                 .addClass('ui-icon-triangle-1-e');
1379                         child.close();
1380                         // force recompute the selection as closing group reset properties
1381                         var selection = self.get_selection();
1382                         $(self).trigger('selected', [selection.ids, this.records]);
1383                     }
1384                 });
1385             }
1386             placeholder.appendChild($row[0]);
1387
1388             var $group_column = $('<th class="oe_list_group_name">').appendTo($row);
1389             // Don't fill this if group_by_no_leaf but no group_by
1390             if (group.grouped_on) {
1391                 var row_data = {};
1392                 row_data[group.grouped_on] = group;
1393                 var group_label = _t("Undefined");
1394                 var group_column = _(self.columns).detect(function (column) {
1395                     return column.id === group.grouped_on; });
1396                 if (group_column) {
1397                     try {
1398                         group_label = group_column.format(row_data, {
1399                             value_if_empty: _t("Undefined"),
1400                             process_modifiers: false
1401                         });
1402                     } catch (e) {
1403                         group_label = _.str.escapeHTML(row_data[group_column.id].value);
1404                     }
1405                 } else {
1406                     group_label = group.value;
1407                     if (group_label instanceof Array) {
1408                         group_label = group_label[1];
1409                     }
1410                     if (group_label === false) {
1411                         group_label = _t('Undefined');
1412                     }
1413                     group_label = _.str.escapeHTML(group_label);
1414                 }
1415                     
1416                 // group_label is html-clean (through format or explicit
1417                 // escaping if format failed), can inject straight into HTML
1418                 $group_column.html(_.str.sprintf(_t("%s (%d)"),
1419                     group_label, group.length));
1420
1421                 if (group.length && group.openable) {
1422                     // Make openable if not terminal group & group_by_no_leaf
1423                     $group_column.prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">');
1424                 } else {
1425                     // Kinda-ugly hack: jquery-ui has no "empty" icon, so set
1426                     // wonky background position to ensure nothing is displayed
1427                     // there but the rest of the behavior is ui-icon's
1428                     $group_column.prepend('<span class="ui-icon" style="float: left; background-position: 150px 150px">');
1429                 }
1430             }
1431             self.indent($group_column, group.level);
1432
1433             if (self.options.selectable) {
1434                 $row.append('<td>');
1435             }
1436             _(self.columns).chain()
1437                 .filter(function (column) { return column.invisible !== '1'; })
1438                 .each(function (column) {
1439                     if (column.meta) {
1440                         // do not do anything
1441                     } else if (column.id in group.aggregates) {
1442                         var r = {};
1443                         r[column.id] = {value: group.aggregates[column.id]};
1444                         $('<td class="oe_number">')
1445                             .html(column.format(r, {process_modifiers: false}))
1446                             .appendTo($row);
1447                     } else {
1448                         $row.append('<td>');
1449                     }
1450                 });
1451             if (self.options.deletable) {
1452                 $row.append('<td class="oe_list_group_pagination">');
1453             }
1454         });
1455         return placeholder;
1456     },
1457     bind_child_events: function (child) {
1458         var $this = $(this),
1459              self = this;
1460         $(child).bind('selected', function (e, _0, _1, deselected) {
1461             // can have selections spanning multiple links
1462             var selection = self.get_selection();
1463             $this.trigger(e, [selection.ids, selection.records, deselected]);
1464         }).bind(this.passthrough_events, function (e) {
1465             // additional positional parameters are provided to trigger as an
1466             // Array, following the event type or event object, but are
1467             // provided to the .bind event handler as *args.
1468             // Convert our *args back into an Array in order to trigger them
1469             // on the group itself, so it can ultimately be forwarded wherever
1470             // it's supposed to go.
1471             var args = Array.prototype.slice.call(arguments, 1);
1472             $this.trigger.call($this, e, args);
1473         });
1474     },
1475     render_dataset: function (dataset) {
1476         var self = this,
1477             list = new (this.view.options.ListType)(this, {
1478                 options: this.options,
1479                 columns: this.columns,
1480                 dataset: dataset,
1481                 records: this.records
1482             });
1483         this.bind_child_events(list);
1484
1485         var view = this.view,
1486            limit = view.limit(),
1487             page = this.datagroup.openable ? this.page : view.page;
1488
1489         var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field";}), 'name');
1490         var options = { offset: page * limit, limit: limit, context: {bin_size: true} };
1491         //TODO xmo: investigate why we need to put the setTimeout
1492         return $.async_when().then(function() {
1493             return dataset.read_slice(fields, options).then(function (records) {
1494                 // FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently
1495                 if (self.records.length) {
1496                     self.records.reset(null, {silent: true});
1497                 }
1498                 if (!self.datagroup.openable) {
1499                     view.configure_pager(dataset);
1500                 } else {
1501                     if (dataset.size() == records.length) {
1502                         // only one page
1503                         self.$row.find('td.oe_list_group_pagination').find('button').remove();
1504                         self.$row.find('td.oe_list_group_pagination').find('span').remove();
1505                     } else {
1506                         var pages = Math.ceil(dataset.size() / limit);
1507                         self.$row
1508                             .find('.oe_list_pager_state')
1509                                 .text(_.str.sprintf(_t("%(page)d/%(page_count)d"), {
1510                                     page: page + 1,
1511                                     page_count: pages
1512                                 }))
1513                             .end()
1514                             .find('button[data-pager-action=previous]')
1515                                 .css('visibility',
1516                                      page === 0 ? 'hidden' : '')
1517                             .end()
1518                             .find('button[data-pager-action=next]')
1519                                 .css('visibility',
1520                                      page === pages - 1 ? 'hidden' : '');
1521                     }
1522                 }
1523
1524                 self.records.add(records, {silent: true});
1525                 list.render();
1526                 if (_.isEmpty(records)) {
1527                     view.no_result();
1528                 }
1529                 return list;
1530             });
1531         });
1532     },
1533     setup_resequence_rows: function (list, dataset) {
1534         // drag and drop enabled if list is not sorted and there is a
1535         // visible column with @widget=handle or "sequence" column in the view.
1536         if ((dataset.sort && dataset.sort())
1537             || !_(this.columns).any(function (column) {
1538                     return column.widget === 'handle'
1539                         || column.name === 'sequence'; })) {
1540             return;
1541         }
1542         var sequence_field = _(this.columns).find(function (c) {
1543             return c.widget === 'handle';
1544         });
1545         var seqname = sequence_field ? sequence_field.name : 'sequence';
1546
1547         // ondrop, move relevant record & fix sequences
1548         list.$current.sortable({
1549             axis: 'y',
1550             items: '> tr[data-id]',
1551             helper: 'clone'
1552         });
1553         if (sequence_field) {
1554             list.$current.sortable('option', 'handle', '.oe_list_field_handle');
1555         }
1556         list.$current.sortable('option', {
1557             start: function (e, ui) {
1558                 ui.placeholder.height(ui.item.height());
1559             },
1560             stop: function (event, ui) {
1561                 var to_move = list.records.get(ui.item.data('id')),
1562                     target_id = ui.item.prev().data('id'),
1563                     from_index = list.records.indexOf(to_move),
1564                     target = list.records.get(target_id);
1565                 if (list.records.at(from_index - 1) == target) {
1566                     return;
1567                 }
1568
1569                 list.records.remove(to_move);
1570                 var to = target_id ? list.records.indexOf(target) + 1 : 0;
1571                 list.records.add(to_move, { at: to });
1572
1573                 // resequencing time!
1574                 var record, index = to,
1575                     // if drag to 1st row (to = 0), start sequencing from 0
1576                     // (exclusive lower bound)
1577                     seq = to ? list.records.at(to - 1).get(seqname) : 0;
1578                 var fct = function (dataset, id, seq) {
1579                     $.async_when().done(function () {
1580                         var attrs = {};
1581                         attrs[seqname] = seq;
1582                         dataset.write(id, attrs);
1583                     });
1584                 };
1585                 while (++seq, (record = list.records.at(index++))) {
1586                     // write are independent from one another, so we can just
1587                     // launch them all at the same time and we don't really
1588                     // give a fig about when they're done
1589                     // FIXME: breaks on o2ms (e.g. Accounting > Financial
1590                     //        Accounting > Taxes > Taxes, child tax accounts)
1591                     //        when synchronous (without setTimeout)
1592                     fct(dataset, record.get('id'), seq);
1593                     record.set(seqname, seq);
1594                 }
1595             }
1596         });
1597     },
1598     render: function (post_render) {
1599         var self = this;
1600         var $el = $('<tbody>');
1601         this.elements = [$el[0]];
1602
1603         this.datagroup.list(
1604             _(this.view.visible_columns).chain()
1605                 .filter(function (column) { return column.tag === 'field';})
1606                 .pluck('name').value(),
1607             function (groups) {
1608                 $el[0].appendChild(
1609                     self.render_groups(groups));
1610                 if (post_render) { post_render(); }
1611             }, function (dataset) {
1612                 self.render_dataset(dataset).then(function (list) {
1613                     self.children[null] = list;
1614                     self.elements =
1615                         [list.$current.replaceAll($el)[0]];
1616                     self.setup_resequence_rows(list, dataset);
1617                 }).always(function() {
1618                     if (post_render) { post_render(); }
1619                 });
1620             });
1621         return $el;
1622     },
1623     /**
1624      * Returns the ids of all selected records for this group, and the records
1625      * themselves
1626      */
1627     get_selection: function () {
1628         var ids = [], records = [];
1629
1630         _(this.children)
1631             .each(function (child) {
1632                 var selection = child.get_selection();
1633                 ids.push.apply(ids, selection.ids);
1634                 records.push.apply(records, selection.records);
1635             });
1636
1637         return {ids: ids, records: records};
1638     },
1639     on_records_reset: function () {
1640         this.children = {};
1641         $(this.elements).remove();
1642     },
1643     get_records: function () {
1644         if (_(this.children).isEmpty()) {
1645             if (!this.datagroup.length) {
1646                 return;
1647             }
1648             return {
1649                 count: this.datagroup.length,
1650                 values: this.datagroup.aggregates
1651             };
1652         }
1653         return _(this.children).chain()
1654             .map(function (child) {
1655                 return child.get_records();
1656             }).flatten().value();
1657     }
1658 });
1659
1660 /**
1661  * Serializes concurrent calls to this asynchronous method. The method must
1662  * return a deferred or promise.
1663  *
1664  * Current-implementation is class-serialized (the mutex is common to all
1665  * instances of the list view). Can be switched to instance-serialized if
1666  * having concurrent list views becomes possible and common.
1667  */
1668 function synchronized(fn) {
1669     var fn_mutex = new $.Mutex();
1670     return function () {
1671         var obj = this;
1672         var args = _.toArray(arguments);
1673         return fn_mutex.exec(function () {
1674             if (obj.isDestroyed()) { return $.when(); }
1675             return fn.apply(obj, args)
1676         });
1677     };
1678 }
1679 var DataGroup =  instance.web.Class.extend({
1680    init: function(parent, model, domain, context, group_by, level) {
1681        this.model = new instance.web.Model(model, context, domain);
1682        this.group_by = group_by;
1683        this.context = context;
1684        this.domain = domain;
1685
1686        this.level = level || 0;
1687    },
1688    list: function (fields, ifGroups, ifRecords) {
1689        var self = this;
1690        if (!_.isEmpty(this.group_by)) {
1691            // ensure group_by fields are read.
1692            fields = _.unique((fields || []).concat(this.group_by));
1693        }
1694        var query = this.model.query(fields).order_by(this.sort).group_by(this.group_by);
1695        $.when(query).done(function (querygroups) {
1696            // leaf node
1697            if (!querygroups) {
1698                var ds = new instance.web.DataSetSearch(self, self.model.name, self.model.context(), self.model.domain());
1699                ds._sort = self.sort;
1700                ifRecords(ds);
1701                return;
1702            }
1703            // internal node
1704            var child_datagroups = _(querygroups).map(function (group) {
1705                var child_context = _.extend(
1706                    {}, self.model.context(), group.model.context());
1707                var child_dg = new DataGroup(
1708                    self, self.model.name, group.model.domain(),
1709                    child_context, group.model._context.group_by,
1710                    self.level + 1);
1711                child_dg.sort = self.sort;
1712                // copy querygroup properties
1713                child_dg.__context = child_context;
1714                child_dg.__domain = group.model.domain();
1715                child_dg.folded = group.get('folded');
1716                child_dg.grouped_on = group.get('grouped_on');
1717                child_dg.length = group.get('length');
1718                child_dg.value = group.get('value');
1719                child_dg.openable = group.get('has_children');
1720                child_dg.aggregates = group.get('aggregates');
1721                return child_dg;
1722            });
1723            ifGroups(child_datagroups);
1724        });
1725    }
1726 });
1727 var StaticDataGroup = DataGroup.extend({
1728    init: function (dataset) {
1729        this.dataset = dataset;
1730    },
1731    list: function (fields, ifGroups, ifRecords) {
1732        ifRecords(this.dataset);
1733    }
1734 });
1735
1736 /**
1737  * @mixin Events
1738  */
1739 var Events = /** @lends Events# */{
1740     /**
1741      * @param {String} event event to listen to on the current object, null for all events
1742      * @param {Function} handler event handler to bind to the relevant event
1743      * @returns this
1744      */
1745     bind: function (event, handler) {
1746         var calls = this['_callbacks'] || (this._callbacks = {});
1747
1748         if (event in calls) {
1749             calls[event].push(handler);
1750         } else {
1751             calls[event] = [handler];
1752         }
1753         return this;
1754     },
1755     /**
1756      * @param {String} event event to unbind on the current object
1757      * @param {function} [handler] specific event handler to remove (otherwise unbind all handlers for the event)
1758      * @returns this
1759      */
1760     unbind: function (event, handler) {
1761         var calls = this._callbacks || {};
1762         if (!(event in calls)) { return this; }
1763         if (!handler) {
1764             delete calls[event];
1765         } else {
1766             var handlers = calls[event];
1767             handlers.splice(
1768                 _(handlers).indexOf(handler),
1769                 1);
1770         }
1771         return this;
1772     },
1773     /**
1774      * @param {String} event
1775      * @returns this
1776      */
1777     trigger: function (event) {
1778         var calls;
1779         if (!(calls = this._callbacks)) { return this; }
1780         var callbacks = (calls[event] || []).concat(calls[null] || []);
1781         for(var i=0, length=callbacks.length; i<length; ++i) {
1782             callbacks[i].apply(this, arguments);
1783         }
1784         return this;
1785     }
1786 };
1787 var Record = instance.web.Class.extend(/** @lends Record# */{
1788     /**
1789      * @constructs Record
1790      * @extends instance.web.Class
1791      * 
1792      * @mixes Events
1793      * @param {Object} [data]
1794      */
1795     init: function (data) {
1796         this.attributes = data || {};
1797     },
1798     /**
1799      * @param {String} key
1800      * @returns {Object}
1801      */
1802     get: function (key) {
1803         return this.attributes[key];
1804     },
1805     /**
1806      * @param key
1807      * @param value
1808      * @param {Object} [options]
1809      * @param {Boolean} [options.silent=false]
1810      * @returns {Record}
1811      */
1812     set: function (key, value, options) {
1813         options = options || {};
1814         var old_value = this.attributes[key];
1815         if (old_value === value) {
1816             return this;
1817         }
1818         this.attributes[key] = value;
1819         if (!options.silent) {
1820             this.trigger('change:' + key, this, value, old_value);
1821             this.trigger('change', this, key, value, old_value);
1822         }
1823         return this;
1824     },
1825     /**
1826      * Converts the current record to the format expected by form views:
1827      *
1828      * .. code-block:: javascript
1829      *
1830      *    data: {
1831      *         $fieldname: {
1832      *             value: $value
1833      *         }
1834      *     }
1835      *
1836      *
1837      * @returns {Object} record displayable in a form view
1838      */
1839     toForm: function () {
1840         var form_data = {}, attrs = this.attributes;
1841         for(var k in attrs) {
1842             form_data[k] = {value: attrs[k]};
1843         }
1844
1845         return {data: form_data};
1846     },
1847     /**
1848      * Converts the current record to a format expected by context evaluations
1849      * (identical to record.attributes, except m2o fields are their integer
1850      * value rather than a pair)
1851      */
1852     toContext: function () {
1853         var output = {}, attrs = this.attributes;
1854         for(var k in attrs) {
1855             var val = attrs[k];
1856             if (typeof val !== 'object') {
1857                 output[k] = val;
1858             } else if (val instanceof Array) {
1859                 output[k] = val[0];
1860             } else {
1861                 throw new Error(_.str.sprintf(_t("Can't convert value %s to context"), val));
1862             }
1863         }
1864         return output;
1865     }
1866 });
1867 Record.include(Events);
1868 var Collection = instance.web.Class.extend(/** @lends Collection# */{
1869     /**
1870      * Smarter collections, with events, very strongly inspired by Backbone's.
1871      *
1872      * Using a "dumb" array of records makes synchronization between the
1873      * various serious 
1874      *
1875      * @constructs Collection
1876      * @extends instance.web.Class
1877      * 
1878      * @mixes Events
1879      * @param {Array} [records] records to initialize the collection with
1880      * @param {Object} [options]
1881      */
1882     init: function (records, options) {
1883         options = options || {};
1884         _.bindAll(this, '_onRecordEvent');
1885         this.length = 0;
1886         this.records = [];
1887         this._byId = {};
1888         this._proxies = {};
1889         this._key = options.key;
1890         this._parent = options.parent;
1891
1892         if (records) {
1893             this.add(records);
1894         }
1895     },
1896     /**
1897      * @param {Object|Array} record
1898      * @param {Object} [options]
1899      * @param {Number} [options.at]
1900      * @param {Boolean} [options.silent=false]
1901      * @returns this
1902      */
1903     add: function (record, options) {
1904         options = options || {};
1905         var records = record instanceof Array ? record : [record];
1906
1907         for(var i=0, length=records.length; i<length; ++i) {
1908             var instance_ = (records[i] instanceof Record) ? records[i] : new Record(records[i]);
1909             instance_.bind(null, this._onRecordEvent);
1910             this._byId[instance_.get('id')] = instance_;
1911             if (options.at === undefined || options.at === null) {
1912                 this.records.push(instance_);
1913                 if (!options.silent) {
1914                     this.trigger('add', this, instance_, this.records.length-1);
1915                 }
1916             } else {
1917                 var insertion_index = options.at + i;
1918                 this.records.splice(insertion_index, 0, instance_);
1919                 if (!options.silent) {
1920                     this.trigger('add', this, instance_, insertion_index);
1921                 }
1922             }
1923             this.length++;
1924         }
1925         return this;
1926     },
1927
1928     /**
1929      * Get a record by its index in the collection, can also take a group if
1930      * the collection is not degenerate
1931      *
1932      * @param {Number} index
1933      * @param {String} [group]
1934      * @returns {Record|undefined}
1935      */
1936     at: function (index, group) {
1937         if (group) {
1938             var groups = group.split('.');
1939             return this._proxies[groups[0]].at(index, groups.join('.'));
1940         }
1941         return this.records[index];
1942     },
1943     /**
1944      * Get a record by its database id
1945      *
1946      * @param {Number} id
1947      * @returns {Record|undefined}
1948      */
1949     get: function (id) {
1950         if (!_(this._proxies).isEmpty()) {
1951             var record = null;
1952             _(this._proxies).detect(function (proxy) {
1953                 record = proxy.get(id);
1954                 return record;
1955             });
1956             return record;
1957         }
1958         return this._byId[id];
1959     },
1960     /**
1961      * Builds a proxy (insert/retrieve) to a subtree of the collection, by
1962      * the subtree's group
1963      *
1964      * @param {String} section group path section
1965      * @returns {Collection}
1966      */
1967     proxy: function (section) {
1968         this._proxies[section] = new Collection(null, {
1969             parent: this,
1970             key: section
1971         }).bind(null, this._onRecordEvent);
1972         return this._proxies[section];
1973     },
1974     /**
1975      * @param {Array} [records]
1976      * @returns this
1977      */
1978     reset: function (records, options) {
1979         options = options || {};
1980         _(this._proxies).each(function (proxy) {
1981             proxy.reset();
1982         });
1983         this._proxies = {};
1984         _(this.records).invoke('unbind', null, this._onRecordEvent);
1985         this.length = 0;
1986         this.records = [];
1987         this._byId = {};
1988         if (records) {
1989             this.add(records);
1990         }
1991         if (!options.silent) {
1992             this.trigger('reset', this);
1993         }
1994         return this;
1995     },
1996     /**
1997      * Removes the provided record from the collection
1998      *
1999      * @param {Record} record
2000      * @returns this
2001      */
2002     remove: function (record) {
2003         var index = this.indexOf(record);
2004         if (index === -1) {
2005             _(this._proxies).each(function (proxy) {
2006                 proxy.remove(record);
2007             });
2008             return this;
2009         }
2010
2011         record.unbind(null, this._onRecordEvent);
2012         this.records.splice(index, 1);
2013         delete this._byId[record.get('id')];
2014         this.length--;
2015         this.trigger('remove', record, this);
2016         return this;
2017     },
2018
2019     _onRecordEvent: function (event) {
2020         switch(event) {
2021         // don't propagate reset events
2022         case 'reset': return;
2023         case 'change:id':
2024             var record = arguments[1];
2025             var new_value = arguments[2];
2026             var old_value = arguments[3];
2027             // [change:id, record, new_value, old_value]
2028             if (this._byId[old_value] === record) {
2029                 delete this._byId[old_value];
2030                 this._byId[new_value] = record;
2031             }
2032             break;
2033         }
2034         this.trigger.apply(this, arguments);
2035     },
2036
2037     // underscore-type methods
2038     find: function (callback) {
2039         var record;
2040         for(var section in this._proxies) {
2041             if (!this._proxies.hasOwnProperty(section)) {
2042                 continue;
2043             }
2044             if ((record = this._proxies[section].find(callback))) {
2045                 return record;
2046             }
2047         }
2048         for(var i=0; i<this.length; ++i) {
2049             record = this.records[i];
2050             if (callback(record)) {
2051                 return record;
2052             }
2053         }
2054     },
2055     each: function (callback) {
2056         for(var section in this._proxies) {
2057             if (this._proxies.hasOwnProperty(section)) {
2058                 this._proxies[section].each(callback);
2059             }
2060         }
2061         for(var i=0; i<this.length; ++i) {
2062             callback(this.records[i]);
2063         }
2064     },
2065     map: function (callback) {
2066         var results = [];
2067         this.each(function (record) {
2068             results.push(callback(record));
2069         });
2070         return results;
2071     },
2072     pluck: function (fieldname) {
2073         return this.map(function (record) {
2074             return record.get(fieldname);
2075         });
2076     },
2077     indexOf: function (record) {
2078         return _(this.records).indexOf(record);
2079     },
2080     succ: function (record, options) {
2081         options = options || {wraparound: false};
2082         var result;
2083         for(var section in this._proxies) {
2084             if (!this._proxies.hasOwnProperty(section)) {
2085                 continue;
2086             }
2087             if ((result = this._proxies[section].succ(record, options))) {
2088                 return result;
2089             }
2090         }
2091         var index = this.indexOf(record);
2092         if (index === -1) { return null; }
2093         var next_index = index + 1;
2094         if (options.wraparound && (next_index === this.length)) {
2095             return this.at(0);
2096         }
2097         return this.at(next_index);
2098     },
2099     pred: function (record, options) {
2100         options = options || {wraparound: false};
2101
2102         var result;
2103         for (var section in this._proxies) {
2104             if (!this._proxies.hasOwnProperty(section)) {
2105                 continue;
2106             }
2107             if ((result = this._proxies[section].pred(record, options))) {
2108                 return result;
2109             }
2110         }
2111
2112         var index = this.indexOf(record);
2113         if (index === -1) { return null; }
2114         var next_index = index - 1;
2115         if (options.wraparound && (next_index === -1)) {
2116             return this.at(this.length - 1);
2117         }
2118         return this.at(next_index);
2119     }
2120 });
2121 Collection.include(Events);
2122 instance.web.list = {
2123     Events: Events,
2124     Record: Record,
2125     Collection: Collection
2126 };
2127 /**
2128  * Registry for column objects used to format table cells (and some other tasks
2129  * e.g. aggregation computations).
2130  *
2131  * Maps a field or button to a Column type via its ``$tag.$widget``,
2132  * ``$tag.$type`` or its ``$tag`` (alone).
2133  *
2134  * This specific registry has a dedicated utility method ``for_`` taking a
2135  * field (from fields_get/fields_view_get.field) and a node (from a view) and
2136  * returning the right object *already instantiated from the data provided*.
2137  *
2138  * @type {instance.web.Registry}
2139  */
2140 instance.web.list.columns = new instance.web.Registry({
2141     'field': 'instance.web.list.Column',
2142     'field.boolean': 'instance.web.list.Boolean',
2143     'field.binary': 'instance.web.list.Binary',
2144     'field.char': 'instance.web.list.Char',
2145     'field.progressbar': 'instance.web.list.ProgressBar',
2146     'field.handle': 'instance.web.list.Handle',
2147     'button': 'instance.web.list.Button',
2148     'field.many2onebutton': 'instance.web.list.Many2OneButton',
2149     'field.reference': 'instance.web.list.Reference',
2150     'field.many2many': 'instance.web.list.Many2Many'
2151 });
2152 instance.web.list.columns.for_ = function (id, field, node) {
2153     var description = _.extend({tag: node.tag}, field, node.attrs);
2154     var tag = description.tag;
2155     var Type = this.get_any([
2156         tag + '.' + description.widget,
2157         tag + '.'+ description.type,
2158         tag
2159     ]);
2160     return new Type(id, node.tag, description);
2161 };
2162
2163 instance.web.list.Column = instance.web.Class.extend({
2164     init: function (id, tag, attrs) {
2165         _.extend(attrs, {
2166             id: id,
2167             tag: tag
2168         });
2169
2170         this.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
2171         delete attrs.modifiers;
2172         _.extend(this, attrs);
2173
2174         if (this.modifiers['tree_invisible']) {
2175             this.invisible = '1';
2176         } else { delete this.invisible; }
2177     },
2178     modifiers_for: function (fields) {
2179         var out = {};
2180         var domain_computer = instance.web.form.compute_domain;
2181
2182         for (var attr in this.modifiers) {
2183             if (!this.modifiers.hasOwnProperty(attr)) { continue; }
2184             var modifier = this.modifiers[attr];
2185             out[attr] = _.isBoolean(modifier)
2186                 ? modifier
2187                 : domain_computer(modifier, fields);
2188         }
2189
2190         return out;
2191     },
2192     to_aggregate: function () {
2193         if (this.type !== 'integer' && this.type !== 'float') {
2194             return {};
2195         }
2196         var aggregation_func = this['group_operator'] || 'sum';
2197         if (!(aggregation_func in this)) {
2198             return {};
2199         }
2200         var C = function (fn, label) {
2201             this['function'] = fn;
2202             this.label = label;
2203         };
2204         C.prototype = this;
2205         return new C(aggregation_func, this[aggregation_func]);
2206     },
2207     /**
2208      *
2209      * @param row_data record whose values should be displayed in the cell
2210      * @param {Object} [options]
2211      * @param {String} [options.value_if_empty=''] what to display if the field's value is ``false``
2212      * @param {Boolean} [options.process_modifiers=true] should the modifiers be computed ?
2213      * @param {String} [options.model] current record's model
2214      * @param {Number} [options.id] current record's id
2215      * @return {String}
2216      */
2217     format: function (row_data, options) {
2218         options = options || {};
2219         var attrs = {};
2220         if (options.process_modifiers !== false) {
2221             attrs = this.modifiers_for(row_data);
2222         }
2223         if (attrs.invisible) { return ''; }
2224
2225         if (!row_data[this.id]) {
2226             return options.value_if_empty === undefined
2227                     ? ''
2228                     : options.value_if_empty;
2229         }
2230         return this._format(row_data, options);
2231     },
2232     /**
2233      * Method to override in order to provide alternative HTML content for the
2234      * cell. Column._format will simply call ``instance.web.format_value`` and
2235      * escape the output.
2236      *
2237      * The output of ``_format`` will *not* be escaped by ``format``, any
2238      * escaping *must be done* by ``format``.
2239      *
2240      * @private
2241      */
2242     _format: function (row_data, options) {
2243         return _.escape(instance.web.format_value(
2244             row_data[this.id].value, this, options.value_if_empty));
2245     }
2246 });
2247 instance.web.list.MetaColumn = instance.web.list.Column.extend({
2248     meta: true,
2249     init: function (id, string) {
2250         this._super(id, '', {string: string});
2251     }
2252 });
2253 instance.web.list.Button = instance.web.list.Column.extend({
2254     /**
2255      * Return an actual ``<button>`` tag
2256      */
2257     format: function (row_data, options) {
2258         options = options || {};
2259         var attrs = {};
2260         if (options.process_modifiers !== false) {
2261             attrs = this.modifiers_for(row_data);
2262         }
2263         if (attrs.invisible) { return ''; }
2264         var template = this.icon && 'ListView.row.button' || 'ListView.row.text_button';
2265         return QWeb.render(template, {
2266             widget: this,
2267             prefix: instance.session.prefix,
2268             disabled: attrs.readonly
2269                 || isNaN(row_data.id.value)
2270                 || instance.web.BufferedDataSet.virtual_id_regex.test(row_data.id.value)
2271         });
2272     }
2273 });
2274 instance.web.list.Boolean = instance.web.list.Column.extend({
2275     /**
2276      * Return a potentially disabled checkbox input
2277      *
2278      * @private
2279      */
2280     _format: function (row_data, options) {
2281         return _.str.sprintf('<input type="checkbox" %s readonly="readonly"/>',
2282                  row_data[this.id].value ? 'checked="checked"' : '');
2283     }
2284 });
2285 instance.web.list.Binary = instance.web.list.Column.extend({
2286     /**
2287      * Return a link to the binary data as a file
2288      *
2289      * @private
2290      */
2291     _format: function (row_data, options) {
2292         var text = _t("Download");
2293         var value = row_data[this.id].value;
2294         var download_url;
2295         if (value && value.substr(0, 10).indexOf(' ') == -1) {
2296             download_url = "data:application/octet-stream;base64," + value;
2297         } else {
2298             download_url = instance.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
2299             if (this.filename) {
2300                 download_url += '&filename_field=' + this.filename;
2301             }
2302         }
2303         if (this.filename && row_data[this.filename]) {
2304             text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value(
2305                     row_data[this.filename].value, {type: 'char'}));
2306         }
2307         return _.template('<a href="<%-href%>"><%-text%></a> (<%-size%>)', {
2308             text: text,
2309             href: download_url,
2310             size: instance.web.binary_to_binsize(value),
2311         });
2312     }
2313 });
2314 instance.web.list.Char = instance.web.list.Column.extend({
2315     replacement: '*',
2316     /**
2317      * If password field, only display replacement characters (if value is
2318      * non-empty)
2319      */
2320     _format: function (row_data, options) {
2321         var value = row_data[this.id].value;
2322         if (value && this.password === 'True') {
2323             return value.replace(/[\s\S]/g, _.escape(this.replacement));
2324         }
2325         return this._super(row_data, options);
2326     }
2327 });
2328 instance.web.list.ProgressBar = instance.web.list.Column.extend({
2329     /**
2330      * Return a formatted progress bar display
2331      *
2332      * @private
2333      */
2334     _format: function (row_data, options) {
2335         return _.template(
2336             '<progress value="<%-value%>" max="100"><%-value%>%</progress>', {
2337                 value: _.str.sprintf("%.0f", row_data[this.id].value || 0)
2338             });
2339     }
2340 });
2341 instance.web.list.Handle = instance.web.list.Column.extend({
2342     init: function () {
2343         this._super.apply(this, arguments);
2344         // Handle overrides the field to not be form-editable.
2345         this.modifiers.readonly = true;
2346     },
2347     /**
2348      * Return styling hooks for a drag handle
2349      *
2350      * @private
2351      */
2352     _format: function (row_data, options) {
2353         return '<div class="oe_list_handle">';
2354     }
2355 });
2356 instance.web.list.Many2OneButton = instance.web.list.Column.extend({
2357     _format: function (row_data, options) {
2358         this.has_value = !!row_data[this.id].value;
2359         this.icon = this.has_value ? 'gtk-yes' : 'gtk-no';
2360         this.string = this.has_value ? _t('View') : _t('Create');
2361         return QWeb.render('Many2OneButton.cell', {
2362             'widget': this,
2363             'prefix': instance.session.prefix,
2364         });
2365     },
2366 });
2367 instance.web.list.Many2Many = instance.web.list.Column.extend({
2368     _format: function (row_data, options) {
2369         if (!_.isEmpty(row_data[this.id].value)) {
2370             // If value, use __display version for printing
2371             row_data[this.id] = row_data[this.id + '__display'];
2372         }
2373         return this._super(row_data, options);
2374     }
2375 });
2376 instance.web.list.Reference = instance.web.list.Column.extend({
2377     _format: function (row_data, options) {
2378         if (!_.isEmpty(row_data[this.id].value)) {
2379             // If value, use __display version for printing
2380             if (!!row_data[this.id + '__display']) {
2381                 row_data[this.id] = row_data[this.id + '__display'];
2382             } else {
2383                 row_data[this.id] = {'value': ''};
2384             }
2385         }
2386         return this._super(row_data, options);
2387     }
2388 });
2389 })();
2390 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: