1 openerp.base.list = function (openerp) {
2 openerp.base.views.add('list', 'openerp.base.ListView');
5 * Formats the rendring of a given value based on its field type
7 * @param {Object} row_data record whose values should be displayed in the cell
8 * @param {Object} column column descriptor
9 * @param {"button"|"field"} column.tag base control type
10 * @param {String} column.type widget type for a field control
11 * @param {String} [column.string] button label
12 * @param {String} [column.icon] button icon
13 * @param {String} [value_if_empty=''] what to display if the field's value is ``false``
15 render_cell: function (row_data, column, value_if_empty) {
16 var attrs = column.modifiers_for(row_data);
17 if (attrs.invisible) { return ''; }
18 if (column.tag === 'button') {
20 '<button type="button" title="', column.string || '', '">',
21 '<img src="/base/static/src/img/icons/', column.icon, '.png"',
22 ' alt="', column.string || '', '"/>',
27 var record = row_data[column.id];
28 if (record.value === false) {
29 return value_if_empty === undefined ? '' : value_if_empty;
31 switch (column.widget || column.type) {
33 // name_get value format
34 return record.value[1];
36 return _.sprintf("%02d:%02d",
37 Math.floor(record.value),
38 Math.round((record.value % 1) * 60));
41 '<progress value="%.2f" max="100.0">%.2f%%</progress>',
42 record.value, record.value);
48 openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListView# */ {
50 // records can be selected one by one
52 // list rows can be deleted
54 // whether the column headers should be displayed
56 // display addition button, with that label
58 // whether the list view can be sorted, note that once a view has been
59 // sorted it can not be reordered anymore
61 // whether the view rows can be reordered (via vertical drag & drop)
65 * Core class for list-type displays.
67 * As a view, needs a number of view-related parameters to be correctly
68 * instantiated, provides options and overridable methods for behavioral
71 * See constructor parameters and method documentations for information on
72 * the default behaviors and possible options for the list view.
76 * @param session An OpenERP session object
77 * @param element_id the id of the DOM elements this view should link itself to
78 * @param {openerp.base.DataSet} dataset the dataset the view should work with
79 * @param {String} view_id the listview's identifier, if any
80 * @param {Object} options A set of options used to configure the view
81 * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox)
82 * @param {Boolean} [options.header=true] should the list's header be displayed
83 * @param {Boolean} [options.deletable=true] are the list rows deletable
84 * @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.
85 * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
86 * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
88 * @borrows openerp.base.ActionExecutor#execute_action as #execute_action
90 init: function(view_manager, session, element_id, dataset, view_id, options) {
91 this._super(session, element_id);
92 this.view_manager = view_manager || new openerp.base.NullViewManager();
93 this.dataset = dataset;
94 this.model = dataset.model;
95 this.view_id = view_id;
99 this.options = _.extend({}, this.defaults, options || {});
100 this.flags = this.view_manager.flags || {};
102 this.set_groups(new openerp.base.ListView.Groups(this));
104 if (this.dataset instanceof openerp.base.DataSetStatic) {
105 this.groups.datagroup = new openerp.base.StaticDataGroup(this.dataset);
111 * Retrieves the view's number of records per page (|| section)
113 * options > defaults > view_manager.action.limit > indefinite
115 * @returns {Number|null}
118 if (this._limit === undefined) {
119 this._limit = (this.options.limit
120 || this.defaults.limit
121 || (this.view_manager.action || {}).limit
127 * Set a custom Group construct as the root of the List View.
129 * @param {openerp.base.ListView.Groups} groups
131 set_groups: function (groups) {
134 $(this.groups).unbind("selected deleted action row_link");
138 this.groups = groups;
139 $(this.groups).bind({
140 'selected': function (e, ids, records) {
141 self.do_select(ids, records);
143 'deleted': function (e, ids) {
146 'action': function (e, action_name, id, callback) {
147 self.do_action(action_name, id, callback);
149 'row_link': function (e, id, dataset) {
150 self.do_activate_record(dataset.index, id, dataset);
155 * View startup method, the default behavior is to set the ``oe-listview``
156 * class on its root element and to perform an RPC load call.
158 * @returns {$.Deferred} loading promise
161 this.$element.addClass('oe-listview');
162 return this.reload_view();
165 * Called after loading the list view's description, sets up such things
166 * as the view table's columns, renders the table itself and hooks up the
167 * various table-level and row-level DOM events (action buttons, deletion
168 * buttons, selection of records, [New] button, selection of a given
171 * Sets up the following:
173 * * Processes arch and fields to generate a complete field descriptor for each field
174 * * Create the table itself and allocate visible columns
175 * * Hook in the top-level (header) [New|Add] and [Delete] button
176 * * Sets up showing/hiding the top-level [Delete] button based on records being selected or not
177 * * Sets up event handlers for action buttons and per-row deletion button
178 * * Hooks global callback for clicking on a row
179 * * Sets up its sidebar, if any
181 * @param {Object} data wrapped fields_view_get result
182 * @param {Object} data.fields_view fields_view_get result (processed)
183 * @param {Object} data.fields_view.fields mapping of fields for the current model
184 * @param {Object} data.fields_view.arch current list view descriptor
185 * @param {Boolean} grouped Is the list view grouped
187 on_loaded: function(data, grouped) {
189 this.fields_view = data.fields_view;
190 //this.log(this.fields_view);
191 this.name = "" + this.fields_view.arch.attrs.string;
193 this.setup_columns(this.fields_view.fields, grouped);
195 if (!this.fields_view.sorted) { this.fields_view.sorted = {}; }
197 this.$element.html(QWeb.render("ListView", this));
200 this.$element.find('.oe-list-add')
201 .click(this.do_add_record)
202 .attr('disabled', grouped && this.options.editable);
203 this.$element.find('.oe-list-delete')
204 .attr('disabled', true)
205 .click(this.do_delete_selected);
206 this.$element.find('thead').delegate('th[data-id]', 'click', function (e) {
209 self.dataset.sort($(this).data('id'));
211 // TODO: should only reload content (and set the right column to a sorted display state)
212 self.reload_content();
215 this.$element.find('.oe-list-pager')
216 .delegate('button', 'click', function () {
218 switch ($this.data('pager-action')) {
220 self.page = 0; break;
222 self.page = Math.floor(
223 self.dataset.ids.length / self.limit());
226 self.page += 1; break;
228 self.page -= 1; break;
230 self.reload_content();
231 }).find('.oe-pager-state')
232 .click(function (e) {
236 var $select = $('<select>')
237 .appendTo($this.empty())
238 .click(function (e) {e.stopPropagation();})
239 .append('<option value="80">80</option>' +
240 '<option value="100">100</option>' +
241 '<option value="200">200</option>' +
242 '<option value="500">500</option>' +
243 '<option value="NaN">Unlimited</option>')
244 .change(function () {
245 var val = parseInt($select.val(), 10);
246 self._limit = (isNaN(val) ? null : val);
248 self.reload_content();
250 .val(self._limit || 'NaN');
253 this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar);
256 * Configures the ListView pager based on the provided dataset's information
258 * Horrifying side-effect: sets the dataset's data on this.dataset?
260 * @param {openerp.base.DataSet} dataset
262 configure_pager: function (dataset) {
263 this.dataset.ids = dataset.ids;
265 var limit = this.limit(),
266 total = dataset.ids.length,
267 first = (this.page * limit),
269 if (!limit || (total - first) < limit) {
272 last = first + limit;
274 this.$element.find('span.oe-pager-state').empty().text(_.sprintf(
275 "[%d to %d] of %d", first + 1, last, total));
278 .find('button[data-pager-action=first], button[data-pager-action=previous]')
279 .attr('disabled', this.page === 0)
281 .find('button[data-pager-action=last], button[data-pager-action=next]')
282 .attr('disabled', last === total);
285 * Sets up the listview's columns: merges view and fields data, move
286 * grouped-by columns to the front of the columns list and make them all
289 * @param {Object} fields fields_view_get's fields section
290 * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
292 setup_columns: function (fields, grouped) {
293 var domain_computer = openerp.base.form.compute_domain;
295 var noop = function () { return {}; };
296 var field_to_column = function (field) {
297 var name = field.attrs.name;
298 var column = _.extend({id: name, tag: field.tag},
299 field.attrs, fields[name]);
300 // modifiers computer
301 if (column.modifiers) {
302 var modifiers = JSON.parse(column.modifiers);
303 column.modifiers_for = function (fields) {
304 if (!modifiers.invisible) {
308 'invisible': domain_computer(modifiers.invisible, fields)
311 if (modifiers['tree_invisible']) {
312 column.invisible = '1';
315 column.modifiers_for = noop;
320 this.columns.splice(0, this.columns.length);
321 this.columns.push.apply(
323 _(this.fields_view.arch.children).map(field_to_column));
325 this.columns.unshift({
326 id: '_group', tag: '', string: "Group", meta: true,
327 modifiers_for: function () { return {}; }
329 id: '_count', tag: '', string: '#', meta: true,
330 modifiers_for: function () { return {}; }
334 this.visible_columns = _.filter(this.columns, function (column) {
335 return column.invisible !== '1';
338 this.aggregate_columns = _(this.visible_columns)
339 .map(function (column) {
340 if (column.type !== 'integer' && column.type !== 'float') {
343 var aggregation_func = column['group_operator'] || 'sum';
345 if (!column[aggregation_func]) {
352 'function': aggregation_func,
353 label: column[aggregation_func]
358 * Used to handle a click on a table row, if no other handler caught the
361 * The default implementation asks the list view's view manager to switch
362 * to a different view (by calling
363 * :js:func:`~openerp.base.ViewManager.on_mode_switch`), using the
364 * provided record index (within the current list view's dataset).
366 * If the index is null, ``switch_to_record`` asks for the creation of a
369 * @param {Number|void} index the record index (in the current dataset) to switch to
370 * @param {String} [view="form"] the view type to switch to
372 select_record:function (index, view) {
373 view = view || 'form';
374 this.dataset.index = index;
375 _.delay(_.bind(function () {
376 if(this.view_manager) {
377 this.view_manager.on_mode_switch(view);
381 do_show: function () {
382 this.$element.show();
384 this.$element.find('table').append(
385 this.groups.apoptosis().render());
388 this.view_manager.sidebar.do_refresh(true);
390 do_hide: function () {
391 this.$element.hide();
395 * Reloads the list view based on the current settings (dataset & al)
397 * @param {Boolean} [grouped] Should the list be displayed grouped
399 reload_view: function (grouped) {
401 var callback = function (field_view_get) {
402 self.on_loaded(field_view_get, grouped);
404 if (this.embedded_view) {
405 return $.Deferred().then(callback).resolve({fields_view: this.embedded_view});
407 return this.rpc('/base/listview/load', {
409 view_id: this.view_id,
410 context: this.dataset.get_context(),
411 toolbar: !!this.flags.sidebar
416 * re-renders the content of the list view
418 reload_content: function () {
419 this.$element.find('table').append(
420 this.groups.apoptosis().render(
421 $.proxy(this, 'compute_aggregates')));
424 * Event handler for a search, asks for the computation/folding of domains
425 * and contexts (and group-by), then reloads the view's content.
427 * @param {Array} domains a sequence of literal and non-literal domains
428 * @param {Array} contexts a sequence of literal and non-literal contexts
429 * @param {Array} groupbys a sequence of literal and non-literal group-by contexts
430 * @returns {$.Deferred} fold request evaluation promise
432 do_search: function (domains, contexts, groupbys) {
433 return this.rpc('/base/session/eval_domain_and_context', {
436 group_by_seq: groupbys
437 }, $.proxy(this, 'do_actual_search'));
440 * Handler for the result of eval_domain_and_context, actually perform the
443 * @param {Object} results results of evaluating domain and process for a search
445 do_actual_search: function (results) {
446 this.dataset.context = results.context;
447 this.dataset.domain = results.domain;
448 this.groups.datagroup = new openerp.base.DataGroup(
449 this.session, this.model,
450 results.domain, results.context,
452 this.groups.datagroup.sort = this.dataset._sort;
454 if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
455 results.group_by = null;
458 this.reload_view(!!results.group_by).then(
459 $.proxy(this, 'reload_content'));
462 * Handles the signal to delete lines from the records list
464 * @param {Array} ids the ids of the records to delete
466 do_delete: function (ids) {
471 return $.when(this.dataset.unlink(ids)).then(function () {
472 self.groups.drop_records(ids);
476 * Handles the signal indicating that a new record has been selected
478 * @param {Array} ids selected record ids
479 * @param {Array} records selected record values
481 do_select: function (ids, records) {
482 this.$element.find('.oe-list-delete')
483 .attr('disabled', !ids.length);
485 if (!records.length) {
486 this.compute_aggregates();
489 this.compute_aggregates(_(records).map(function (record) {
490 return {count: 1, values: record};
494 * Handles action button signals on a record
496 * @param {String} name action name
497 * @param {Object} id id of the record the action should be called on
498 * @param {Function} callback should be called after the action is executed, if non-null
500 do_action: function (name, id, callback) {
501 var action = _.detect(this.columns, function (field) {
502 return field.name === name;
504 if (!action) { return; }
505 this.execute_action(action, this.dataset, this.session.action_manager, id, callback);
508 * Handles the activation of a record (clicking on it)
510 * @param {Number} index index of the record in the dataset
511 * @param {Object} id identifier of the activated record
512 * @param {openerp.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups)
514 do_activate_record: function (index, id, dataset) {
516 _.extend(this.dataset, {
517 domain: dataset.domain,
518 context: dataset.get_context()
519 }).read_slice([], 0, false, function () {
520 self.select_record(index);
524 * Handles signal for the addition of a new record (can be a creation,
525 * can be the addition from a remote source, ...)
527 * The default implementation is to switch to a new record on the form view
529 do_add_record: function () {
530 this.select_record(null);
533 * Handles deletion of all selected lines
535 do_delete_selected: function () {
536 this.do_delete(this.groups.get_selection().ids);
539 * Computes the aggregates for the current list view, either on the
540 * records provided or on the records of the internal
541 * :js:class:`~openerp.base.ListView.Group`, by calling
542 * :js:func:`~openerp.base.ListView.group.get_records`.
544 * Then displays the aggregates in the table through
545 * :js:method:`~openerp.base.ListView.display_aggregates`.
547 * @param {Array} [records]
549 compute_aggregates: function (records) {
550 var columns = _(this.aggregate_columns).filter(function (column) {
551 return column['function']; });
553 if (_.isEmpty(columns)) { return; }
555 if (_.isEmpty(records)) {
556 records = this.groups.get_records();
559 var count = 0, sums = {};
560 _(columns).each(function (column) { sums[column.field] = 0; });
561 _(records).each(function (record) {
562 count += record.count || 1;
563 _(columns).each(function (column) {
564 var field = column.field;
565 switch (column['function']) {
567 sums[field] += record.values[field];
570 sums[field] += record.count * record.values[field];
577 _(columns).each(function (column) {
578 var field = column.field;
579 switch (column['function']) {
581 aggregates[field] = sums[field];
584 aggregates[field] = sums[field] / count;
589 this.display_aggregates(aggregates);
591 display_aggregates: function (aggregation) {
592 var $footer_cells = this.$element.find('.oe-list-footer');
593 _(this.aggregate_columns).each(function (column) {
594 if (!column['function']) {
597 var pattern = (column.type == 'integer') ? '%d' : '%.2f';
598 $footer_cells.filter(_.sprintf('[data-field=%s]', column.field))
599 .text(_.sprintf(pattern, aggregation[column.field]));
602 // TODO: implement reorder (drag and drop rows)
604 openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List# */{
606 * List display for the ListView, handles basic DOM events and transforms
607 * them in the relevant higher-level events, to which the list view (or
608 * other consumers) can subscribe.
610 * Events on this object are registered via jQuery.
615 * Triggered when a row is selected (using check boxes), provides an
616 * array of ids of all the selected records.
618 * Triggered when deletion buttons are hit, provide an array of ids of
619 * all the records being marked for suppression.
621 * Triggered when an action button is clicked, provides two parameters:
623 * * The name of the action to execute (as a string)
624 * * The id of the record to execute the action on
626 * Triggered when a row of the table is clicked, provides the index (in
627 * the rows array) and id of the selected record to the handle function.
630 * @param {Object} opts display options, identical to those of :js:class:`openerp.base.ListView`
632 init: function (group, opts) {
635 this.view = group.view;
637 this.options = opts.options;
638 this.columns = opts.columns;
639 this.dataset = opts.dataset;
640 this.rows = opts.rows;
642 this.$_element = $('<tbody class="ui-widget-content">')
643 .appendTo(document.body)
644 .delegate('th.oe-record-selector', 'click', function (e) {
646 var selection = self.get_selection();
648 'selected', [selection.ids, selection.records]);
650 .delegate('td.oe-record-delete button', 'click', function (e) {
652 var $row = $(e.target).closest('tr');
653 $(self).trigger('deleted', [[self.row_id($row)]]);
655 .delegate('td.oe-field-cell button', 'click', function (e) {
657 var $target = $(e.currentTarget),
658 field = $target.closest('td').data('field'),
659 $row = $target.closest('tr'),
660 record_id = self.row_id($row),
661 index = self.row_position($row);
663 $(self).trigger('action', [field, record_id, function () {
664 self.reload_record(index, true);
667 .delegate('tr', 'click', function (e) {
669 self.dataset.index = self.row_position(e.currentTarget);
673 row_clicked: function () {
676 [this.rows[this.dataset.index].data.id.value,
679 render: function () {
681 this.$current.remove();
683 this.$current = this.$_element.clone(true);
684 this.$current.empty().append(
685 QWeb.render('ListView.rows', _.extend({
686 render_cell: openerp.base.list.render_cell}, this)));
689 * Gets the ids of all currently selected records, if any
690 * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves.
692 get_selection: function () {
693 if (!this.options.selectable) {
696 var rows = this.rows;
697 var result = {ids: [], records: []};
698 this.$current.find('th.oe-record-selector input:checked')
699 .closest('tr').each(function () {
701 _(rows[$(this).data('index')].data).each(function (obj, key) {
702 record[key] = obj.value;
704 result.ids.push(record.id);
705 result.records.push(record);
710 * Returns the index of the row in the list of rows.
712 * @param {Object} row the selected row
713 * @returns {Number} the position of the row in this.rows
715 row_position: function (row) {
716 return $(row).data('index');
719 * Returns the identifier of the object displayed in the provided table
722 * @param {Object} row the selected table row
723 * @returns {Number|String} the identifier of the row's object
725 row_id: function (row) {
726 return this.rows[this.row_position(row)].data.id.value;
729 * Death signal, cleans up list
731 apoptosis: function () {
732 if (!this.$current) { return; }
733 this.$current.remove();
734 this.$current = null;
735 this.$_element.remove();
737 get_records: function () {
738 return _(this.rows).map(function (row) {
740 _(row.data).each(function (obj, key) {
741 record[key] = obj.value;
743 return {count: 1, values: record};
747 * Transforms a record from what is returned by a dataset read (a simple
748 * mapping of ``$fieldname: $value``) to the format expected by list rows
757 * This format allows for the insertion of a bunch of metadata (names,
760 * @param {Object} record original record, in dataset format
761 * @returns {Object} record displayable in a form or list view
763 transform_record: function (record) {
764 // TODO: colors handling
766 form_record = {data: form_data};
768 _(record).each(function (value, key) {
769 form_data[key] = {value: value};
775 * Reloads the record at index ``row_index`` in the list's rows.
777 * By default, simply re-renders the record. If the ``fetch`` parameter is
778 * provided and ``true``, will first fetch the record anew.
780 * @param {Number} record_index index of the record to reload
781 * @param {Boolean} fetch fetches the record from remote before reloading it
783 reload_record: function (record_index, fetch) {
787 // save index to restore it later, if already set
788 var old_index = this.dataset.index;
789 this.dataset.index = record_index;
790 read_p = this.dataset.read_index(
791 _.filter(_.pluck(this.columns, 'name'), _.identity),
793 var form_record = self.transform_record(record);
794 self.rows.splice(record_index, 1, form_record);
795 self.dataset.index = old_index;
800 return $.when(read_p).then(function () {
801 self.$current.children().eq(record_index)
802 .replaceWith(self.render_record(record_index)); })
805 * Renders a list record to HTML
807 * @param {Number} record_index index of the record to render in ``this.rows``
808 * @returns {String} QWeb rendering of the selected record
810 render_record: function (record_index) {
811 return QWeb.render('ListView.row', {
812 columns: this.columns,
813 options: this.options,
814 row: this.rows[record_index],
815 row_parity: (record_index % 2 === 0) ? 'even' : 'odd',
816 row_index: record_index,
817 render_cell: openerp.base.list.render_cell
821 * Stops displaying the records matching the provided ids.
823 * @param {Array} ids identifiers of the records to remove
825 drop_records: function (ids) {
828 .map(function (record, index) {
829 return {index: index, id: record.data.id.value};
830 }).filter(function (record) {
831 return _(ids).contains(record.id);
833 .each(function (record) {
834 self.$current.find('tr:eq(' + record.index + ')').remove();
835 self.rows.splice(record.index, 1);
840 openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Groups# */{
841 passtrough_events: 'action deleted row_link',
843 * Grouped display for the ListView. Handles basic DOM events and interacts
844 * with the :js:class:`~openerp.base.DataGroup` bound to it.
846 * Provides events similar to those of
847 * :js:class:`~openerp.base.ListView.List`
849 init: function (view) {
851 this.options = view.options;
852 this.columns = view.columns;
853 this.datagroup = null;
860 make_fragment: function () {
861 return document.createDocumentFragment();
864 * Returns a DOM node after which a new tbody can be inserted, so that it
865 * follows the provided row.
867 * Necessary to insert the result of a new group or list view within an
868 * existing groups render, without losing track of the groups's own
871 * @param {HTMLTableRowElement} row the row after which the caller wants to insert a body
872 * @returns {HTMLTableSectionElement} element after which a tbody can be inserted
874 point_insertion: function (row) {
876 var red_letter_tboday = $row.closest('tbody')[0];
878 var $next_siblings = $row.nextAll();
879 if ($next_siblings.length) {
880 var $root_kanal = $('<tbody>').insertAfter(red_letter_tboday);
882 $root_kanal.append($next_siblings);
883 this.elements.splice(
884 _.indexOf(this.elements, red_letter_tboday),
888 return red_letter_tboday;
890 make_paginator: function () {
892 var $prev = $('<button type="button" data-pager-action="previous"><</button>')
893 .click(function (e) {
897 self.$row.closest('tbody').next()
898 .replaceWith(self.render());
900 var $next = $('<button type="button" data-pager-action="next">></button>')
901 .click(function (e) {
905 self.$row.closest('tbody').next()
906 .replaceWith(self.render());
908 this.$row.children().last()
910 .append('<span class="oe-pager-state"></span>')
913 open: function (point_insertion) {
914 this.render().insertAfter(point_insertion);
915 this.make_paginator();
918 this.$row.children().last().empty();
922 * Prefixes ``$node`` with floated spaces in order to indent it relative
923 * to its own left margin/baseline
925 * @param {jQuery} $node jQuery object to indent
926 * @param {Number} level current nesting level, >= 1
927 * @returns {jQuery} the indentation node created
929 indent: function ($node, level) {
931 .css({'float': 'left', 'white-space': 'pre'})
932 .text(new Array(level).join(' '))
935 render_groups: function (datagroups) {
937 var placeholder = this.make_fragment();
938 _(datagroups).each(function (group) {
939 if (self.children[group.value]) {
940 self.children[group.value].apoptosis();
941 delete self.children[group.value];
943 var child = self.children[group.value] = new openerp.base.ListView.Groups(self.view, {
944 options: self.options,
945 columns: self.columns
947 self.bind_child_events(child);
948 child.datagroup = group;
950 var $row = child.$row = $('<tr>');
951 if (group.openable) {
952 $row.click(function (e) {
953 if (!$row.data('open')) {
954 $row.data('open', true)
955 .find('span.ui-icon')
956 .removeClass('ui-icon-triangle-1-e')
957 .addClass('ui-icon-triangle-1-s');
958 child.open(self.point_insertion(e.currentTarget));
960 $row.removeData('open')
961 .find('span.ui-icon')
962 .removeClass('ui-icon-triangle-1-s')
963 .addClass('ui-icon-triangle-1-e');
968 placeholder.appendChild($row[0]);
970 var $group_column = $('<th class="oe-group-name">').appendTo($row);
971 // Don't fill this if group_by_no_leaf but no group_by
972 if (group.grouped_on) {
974 row_data[group.grouped_on] = group;
975 var group_column = _(self.columns).detect(function (column) {
976 return column.id === group.grouped_on; });
977 $group_column.text(openerp.base.list.render_cell(
978 row_data, group_column, "Undefined"
980 if (group.openable) {
981 // Make openable if not terminal group & group_by_no_leaf
983 .prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">');
986 self.indent($group_column, group.level);
988 $('<td>').text(group.length).appendTo($row);
990 if (self.options.selectable) {
993 _(self.columns).chain()
994 .filter(function (column) {return !column.invisible;})
995 .each(function (column) {
997 // do not do anything
998 } else if (column.id in group.aggregates) {
999 var value = group.aggregates[column.id];
1001 if (column.type === 'integer') {
1003 } else if (column.type === 'float') {
1007 .text(_.sprintf(format, value))
1010 $row.append('<td>');
1013 if (self.options.deletable) {
1014 $row.append('<td class="oe-group-pagination">');
1019 bind_child_events: function (child) {
1020 var $this = $(this),
1022 $(child).bind('selected', function (e) {
1023 // can have selections spanning multiple links
1024 var selection = self.get_selection();
1025 $this.trigger(e, [selection.ids, selection.records]);
1026 }).bind(this.passtrough_events, function (e) {
1027 // additional positional parameters are provided to trigger as an
1028 // Array, following the event type or event object, but are
1029 // provided to the .bind event handler as *args.
1030 // Convert our *args back into an Array in order to trigger them
1031 // on the group itself, so it can ultimately be forwarded wherever
1032 // it's supposed to go.
1033 var args = Array.prototype.slice.call(arguments, 1);
1034 $this.trigger.call($this, e, args);
1037 render_dataset: function (dataset) {
1040 list = new openerp.base.ListView.List(this, {
1041 options: this.options,
1042 columns: this.columns,
1046 this.bind_child_events(list);
1048 var view = this.view,
1049 limit = view.limit(),
1050 d = new $.Deferred(),
1051 page = this.datagroup.openable ? this.page : view.page;
1054 _.filter(_.pluck(_.select(this.columns, function(x) {return x.tag == "field";}), 'name'), _.identity),
1055 page * limit, limit,
1056 function (records) {
1057 if (!self.datagroup.openable) {
1058 view.configure_pager(dataset);
1060 var pages = Math.ceil(dataset.ids.length / limit);
1062 .find('.oe-pager-state')
1063 .text(_.sprintf('%d/%d', page + 1, pages))
1065 .find('button[data-pager-action=previous]')
1066 .attr('disabled', page === 0)
1068 .find('button[data-pager-action=next]')
1069 .attr('disabled', page === pages - 1);
1072 var form_records = _(records).map(
1073 $.proxy(list, 'transform_record'));
1075 rows.splice(0, rows.length);
1076 rows.push.apply(rows, form_records);
1082 render: function (post_render) {
1084 var $element = $('<tbody>');
1085 this.elements = [$element[0]];
1087 this.datagroup.list(
1088 _(this.view.visible_columns).chain()
1089 .filter(function (column) { return column.tag === 'field' })
1090 .pluck('name').value(),
1092 $element[0].appendChild(
1093 self.render_groups(groups));
1094 if (post_render) { post_render(); }
1095 }, function (dataset) {
1096 self.render_dataset(dataset).then(function (list) {
1097 self.children[null] = list;
1099 [list.$current.replaceAll($element)[0]];
1100 if (post_render) { post_render(); }
1106 * Returns the ids of all selected records for this group, and the records
1109 get_selection: function () {
1110 var ids = [], records = [];
1113 .each(function (child) {
1114 var selection = child.get_selection();
1115 ids.push.apply(ids, selection.ids);
1116 records.push.apply(records, selection.records);
1119 return {ids: ids, records: records};
1121 apoptosis: function () {
1122 _(this.children).each(function (child) {
1126 $(this.elements).remove();
1129 get_records: function () {
1130 if (_(this.children).isEmpty()) {
1132 count: this.datagroup.length,
1133 values: this.datagroup.aggregates
1136 return _(this.children).chain()
1137 .map(function (child) {
1138 return child.get_records();
1139 }).flatten().value();
1142 * Stops displaying the records with the linked ids, assumes these records
1143 * were deleted from the DB.
1145 * This is the up-signal from the `deleted` event on groups and lists.
1147 * @param {Array} ids list of identifier of the records to remove.
1149 drop_records: function (ids) {
1150 _.each(this.children, function (child) {
1151 child.drop_records(ids);
1157 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: