1 openerp.base.list = function (openerp) {
2 openerp.base.views.add('list', 'openerp.base.ListView');
3 openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListView# */ {
5 // records can be selected one by one
7 // list rows can be deleted
9 // whether the column headers should be displayed
11 // display addition button, with that label
13 // whether the list view can be sorted, note that once a view has been
14 // sorted it can not be reordered anymore
16 // whether the view rows can be reordered (via vertical drag & drop)
20 * Core class for list-type displays.
22 * As a view, needs a number of view-related parameters to be correctly
23 * instantiated, provides options and overridable methods for behavioral
26 * See constructor parameters and method documentations for information on
27 * the default behaviors and possible options for the list view.
30 * @param parent parent object
31 * @param element_id the id of the DOM elements this view should link itself to
32 * @param {openerp.base.DataSet} dataset the dataset the view should work with
33 * @param {String} view_id the listview's identifier, if any
34 * @param {Object} options A set of options used to configure the view
35 * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox)
36 * @param {Boolean} [options.header=true] should the list's header be displayed
37 * @param {Boolean} [options.deletable=true] are the list rows deletable
38 * @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.
39 * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
40 * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
42 * @borrows openerp.base.ActionExecutor#execute_action as #execute_action
44 init: function(parent, element_id, dataset, view_id, options) {
45 this._super(parent, element_id);
46 this.set_default_options(_.extend({}, this.defaults, options || {}));
47 this.dataset = dataset;
48 this.model = dataset.model;
49 this.view_id = view_id;
53 this.set_groups(new openerp.base.ListView.Groups(this));
55 if (this.dataset instanceof openerp.base.DataSetStatic) {
56 this.groups.datagroup = new openerp.base.StaticDataGroup(this.dataset);
62 * Retrieves the view's number of records per page (|| section)
64 * options > defaults > parent.action.limit > indefinite
66 * @returns {Number|null}
69 if (this._limit === undefined) {
70 this._limit = (this.options.limit
71 || this.defaults.limit
72 || (this.widget_parent.action || {}).limit
78 * Set a custom Group construct as the root of the List View.
80 * @param {openerp.base.ListView.Groups} groups
82 set_groups: function (groups) {
85 $(this.groups).unbind("selected deleted action row_link");
91 'selected': function (e, ids, records) {
92 self.do_select(ids, records);
94 'deleted': function (e, ids) {
97 'action': function (e, action_name, id, callback) {
98 self.do_button_action(action_name, id, callback);
100 'row_link': function (e, id, dataset) {
101 self.do_activate_record(dataset.index, id, dataset);
106 * View startup method, the default behavior is to set the ``oe-listview``
107 * class on its root element and to perform an RPC load call.
109 * @returns {$.Deferred} loading promise
112 this.$element.addClass('oe-listview');
113 return this.reload_view();
116 * Called after loading the list view's description, sets up such things
117 * as the view table's columns, renders the table itself and hooks up the
118 * various table-level and row-level DOM events (action buttons, deletion
119 * buttons, selection of records, [New] button, selection of a given
122 * Sets up the following:
124 * * Processes arch and fields to generate a complete field descriptor for each field
125 * * Create the table itself and allocate visible columns
126 * * Hook in the top-level (header) [New|Add] and [Delete] button
127 * * Sets up showing/hiding the top-level [Delete] button based on records being selected or not
128 * * Sets up event handlers for action buttons and per-row deletion button
129 * * Hooks global callback for clicking on a row
130 * * Sets up its sidebar, if any
132 * @param {Object} data wrapped fields_view_get result
133 * @param {Object} data.fields_view fields_view_get result (processed)
134 * @param {Object} data.fields_view.fields mapping of fields for the current model
135 * @param {Object} data.fields_view.arch current list view descriptor
136 * @param {Boolean} grouped Is the list view grouped
138 on_loaded: function(data, grouped) {
140 this.fields_view = data.fields_view;
141 //this.log(this.fields_view);
142 this.name = "" + this.fields_view.arch.attrs.string;
144 this.setup_columns(this.fields_view.fields, grouped);
146 this.$element.html(QWeb.render("ListView", this));
149 this.$element.find('.oe-list-add')
150 .click(this.do_add_record)
151 .attr('disabled', grouped && this.options.editable);
152 this.$element.find('.oe-list-delete')
153 .attr('disabled', true)
154 .click(this.do_delete_selected);
155 this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) {
159 self.dataset.sort($this.data('id'));
160 if ($this.find('span').length) {
161 $this.find('span').toggleClass(
162 'ui-icon-triangle-1-s ui-icon-triangle-1-n');
164 $this.append('<span class="ui-icon ui-icon-triangle-1-s">')
165 .siblings('.oe-sortable').find('span').remove();
168 self.reload_content();
171 this.$element.find('.oe-list-pager')
172 .delegate('button', 'click', function () {
174 switch ($this.data('pager-action')) {
176 self.page = 0; break;
178 self.page = Math.floor(
179 self.dataset.ids.length / self.limit());
182 self.page += 1; break;
184 self.page -= 1; break;
186 self.reload_content();
187 }).find('.oe-pager-state')
188 .click(function (e) {
192 var $select = $('<select>')
193 .appendTo($this.empty())
194 .click(function (e) {e.stopPropagation();})
195 .append('<option value="80">80</option>' +
196 '<option value="100">100</option>' +
197 '<option value="200">200</option>' +
198 '<option value="500">500</option>' +
199 '<option value="NaN">Unlimited</option>')
200 .change(function () {
201 var val = parseInt($select.val(), 10);
202 self._limit = (isNaN(val) ? null : val);
204 self.reload_content();
206 .val(self._limit || 'NaN');
208 if (!this.sidebar && this.options.sidebar && this.options.sidebar_id) {
209 this.sidebar = new openerp.base.Sidebar(this, this.options.sidebar_id);
210 this.sidebar.start();
211 this.sidebar.add_toolbar(data.fields_view.toolbar);
212 this.set_common_sidebar_sections(this.sidebar);
216 * Configures the ListView pager based on the provided dataset's information
218 * Horrifying side-effect: sets the dataset's data on this.dataset?
220 * @param {openerp.base.DataSet} dataset
222 configure_pager: function (dataset) {
223 this.dataset.ids = dataset.ids;
225 var limit = this.limit(),
226 total = dataset.ids.length,
227 first = (this.page * limit),
229 if (!limit || (total - first) < limit) {
232 last = first + limit;
234 this.$element.find('span.oe-pager-state').empty().text(_.sprintf(
235 "[%d to %d] of %d", first + 1, last, total));
238 .find('button[data-pager-action=first], button[data-pager-action=previous]')
239 .attr('disabled', this.page === 0)
241 .find('button[data-pager-action=last], button[data-pager-action=next]')
242 .attr('disabled', last === total);
245 * Sets up the listview's columns: merges view and fields data, move
246 * grouped-by columns to the front of the columns list and make them all
249 * @param {Object} fields fields_view_get's fields section
250 * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
252 setup_columns: function (fields, grouped) {
253 var domain_computer = openerp.base.form.compute_domain;
255 var noop = function () { return {}; };
256 var field_to_column = function (field) {
257 var name = field.attrs.name;
258 var column = _.extend({id: name, tag: field.tag},
259 field.attrs, fields[name]);
260 // modifiers computer
261 if (column.modifiers) {
262 var modifiers = JSON.parse(column.modifiers);
263 column.modifiers_for = function (fields) {
264 if (!modifiers.invisible) {
268 'invisible': domain_computer(modifiers.invisible, fields)
271 if (modifiers['tree_invisible']) {
272 column.invisible = '1';
275 column.modifiers_for = noop;
280 this.columns.splice(0, this.columns.length);
281 this.columns.push.apply(
283 _(this.fields_view.arch.children).map(field_to_column));
285 this.columns.unshift({
286 id: '_group', tag: '', string: "Group", meta: true,
287 modifiers_for: function () { return {}; }
289 id: '_count', tag: '', string: '#', meta: true,
290 modifiers_for: function () { return {}; }
294 this.visible_columns = _.filter(this.columns, function (column) {
295 return column.invisible !== '1';
298 this.aggregate_columns = _(this.visible_columns)
299 .map(function (column) {
300 if (column.type !== 'integer' && column.type !== 'float') {
303 var aggregation_func = column['group_operator'] || 'sum';
305 return _.extend({}, column, {
306 'function': aggregation_func,
307 label: column[aggregation_func]
312 * Used to handle a click on a table row, if no other handler caught the
315 * The default implementation asks the list view's view manager to switch
316 * to a different view (by calling
317 * :js:func:`~openerp.base.ViewManager.on_mode_switch`), using the
318 * provided record index (within the current list view's dataset).
320 * If the index is null, ``switch_to_record`` asks for the creation of a
323 * @param {Number|void} index the record index (in the current dataset) to switch to
324 * @param {String} [view="form"] the view type to switch to
326 select_record:function (index, view) {
327 view = view || 'form';
328 this.dataset.index = index;
329 _.delay(_.bind(function () {
330 this.do_switch_view(view);
333 do_show: function () {
334 this.$element.show();
336 this.sidebar.$element.show();
339 this.$element.find('.oe-listview-content').append(
340 this.groups.apoptosis().render());
344 do_hide: function () {
345 this.$element.hide();
347 this.sidebar.$element.hide();
352 * Reloads the list view based on the current settings (dataset & al)
354 * @param {Boolean} [grouped] Should the list be displayed grouped
355 * @param {Object} [context] context to send the server while loading the view
357 reload_view: function (grouped, context) {
359 var callback = function (field_view_get) {
360 self.on_loaded(field_view_get, grouped);
362 if (this.embedded_view) {
363 return $.Deferred().then(callback).resolve({fields_view: this.embedded_view});
365 return this.rpc('/base/listview/load', {
367 view_id: this.view_id,
368 context: this.dataset.get_context(context),
369 toolbar: this.options.sidebar
374 * re-renders the content of the list view
376 reload_content: function () {
377 this.$element.find('.oe-listview-content').append(
378 this.groups.apoptosis().render(
379 $.proxy(this, 'compute_aggregates')));
382 * Event handler for a search, asks for the computation/folding of domains
383 * and contexts (and group-by), then reloads the view's content.
385 * @param {Array} domains a sequence of literal and non-literal domains
386 * @param {Array} contexts a sequence of literal and non-literal contexts
387 * @param {Array} groupbys a sequence of literal and non-literal group-by contexts
388 * @returns {$.Deferred} fold request evaluation promise
390 do_search: function (domains, contexts, groupbys) {
391 return this.rpc('/base/session/eval_domain_and_context', {
394 group_by_seq: groupbys
395 }, $.proxy(this, 'do_actual_search'));
398 * Handler for the result of eval_domain_and_context, actually perform the
401 * @param {Object} results results of evaluating domain and process for a search
403 do_actual_search: function (results) {
404 this.groups.datagroup = new openerp.base.DataGroup(
406 this.dataset.get_domain(results.domain),
407 this.dataset.get_context(results.context),
409 this.groups.datagroup.sort = this.dataset._sort;
411 if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
412 results.group_by = null;
415 this.reload_view(!!results.group_by, results.context).then(
416 $.proxy(this, 'reload_content'));
419 * Handles the signal to delete lines from the records list
421 * @param {Array} ids the ids of the records to delete
423 do_delete: function (ids) {
428 return $.when(this.dataset.unlink(ids)).then(function () {
429 self.groups.drop_records(ids);
430 self.compute_aggregates();
434 * Handles the signal indicating that a new record has been selected
436 * @param {Array} ids selected record ids
437 * @param {Array} records selected record values
439 do_select: function (ids, records) {
440 this.$element.find('.oe-list-delete')
441 .attr('disabled', !ids.length);
443 if (!records.length) {
444 this.compute_aggregates();
447 this.compute_aggregates(_(records).map(function (record) {
448 return {count: 1, values: record};
452 * Handles action button signals on a record
454 * @param {String} name action name
455 * @param {Object} id id of the record the action should be called on
456 * @param {Function} callback should be called after the action is executed, if non-null
458 do_button_action: function (name, id, callback) {
460 action = _.detect(this.columns, function (field) {
461 return field.name === name;
463 if (!action) { return; }
464 this.execute_action(action, this.dataset, id, function () {
465 $.when(callback.apply(this, arguments).then(function () {
466 self.compute_aggregates();
471 * Handles the activation of a record (clicking on it)
473 * @param {Number} index index of the record in the dataset
474 * @param {Object} id identifier of the activated record
475 * @param {openerp.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups)
477 do_activate_record: function (index, id, dataset) {
479 this.dataset.read_slice({
480 context: dataset.get_context(),
481 domain: dataset.get_domain()
483 self.select_record(index);
487 * Handles signal for the addition of a new record (can be a creation,
488 * can be the addition from a remote source, ...)
490 * The default implementation is to switch to a new record on the form view
492 do_add_record: function () {
493 this.select_record(null);
496 * Handles deletion of all selected lines
498 do_delete_selected: function () {
499 this.do_delete(this.groups.get_selection().ids);
502 * Computes the aggregates for the current list view, either on the
503 * records provided or on the records of the internal
504 * :js:class:`~openerp.base.ListView.Group`, by calling
505 * :js:func:`~openerp.base.ListView.group.get_records`.
507 * Then displays the aggregates in the table through
508 * :js:method:`~openerp.base.ListView.display_aggregates`.
510 * @param {Array} [records]
512 compute_aggregates: function (records) {
513 var columns = _(this.aggregate_columns).filter(function (column) {
514 return column['function']; });
515 if (_.isEmpty(columns)) { return; }
517 if (_.isEmpty(records)) {
518 records = this.groups.get_records();
521 var count = 0, sums = {};
522 _(columns).each(function (column) {
523 switch (column['function']) {
525 sums[column.id] = -Infinity;
528 sums[column.id] = Infinity;
534 _(records).each(function (record) {
535 count += record.count || 1;
536 _(columns).each(function (column) {
537 var field = column.id,
538 value = record.values[field];
539 switch (column['function']) {
541 sums[field] += value;
544 sums[field] += record.count * value;
547 if (sums[field] > value) {
552 if (sums[field] < value) {
561 _(columns).each(function (column) {
562 var field = column.id;
563 switch (column['function']) {
565 aggregates[field] = {value: sums[field] / count};
568 aggregates[field] = {value: sums[field]};
572 this.display_aggregates(aggregates);
574 display_aggregates: function (aggregation) {
575 var $footer_cells = this.$element.find('.oe-list-footer');
576 _(this.aggregate_columns).each(function (column) {
577 if (!column['function']) {
581 $footer_cells.filter(_.sprintf('[data-field=%s]', column.id))
582 .html(openerp.base.format_cell(aggregation, column));
585 // TODO: implement reorder (drag and drop rows)
587 openerp.base.ListView.List = openerp.base.Class.extend( /** @lends openerp.base.ListView.List# */{
589 * List display for the ListView, handles basic DOM events and transforms
590 * them in the relevant higher-level events, to which the list view (or
591 * other consumers) can subscribe.
593 * Events on this object are registered via jQuery.
598 * Triggered when a row is selected (using check boxes), provides an
599 * array of ids of all the selected records.
601 * Triggered when deletion buttons are hit, provide an array of ids of
602 * all the records being marked for suppression.
604 * Triggered when an action button is clicked, provides two parameters:
606 * * The name of the action to execute (as a string)
607 * * The id of the record to execute the action on
609 * Triggered when a row of the table is clicked, provides the index (in
610 * the rows array) and id of the selected record to the handle function.
613 * @param {Object} opts display options, identical to those of :js:class:`openerp.base.ListView`
615 init: function (group, opts) {
618 this.view = group.view;
619 this.session = this.view.session;
621 this.options = opts.options;
622 this.columns = opts.columns;
623 this.dataset = opts.dataset;
624 this.rows = opts.rows;
626 this.$_element = $('<tbody class="ui-widget-content">')
627 .appendTo(document.body)
628 .delegate('th.oe-record-selector', 'click', function (e) {
630 var selection = self.get_selection();
632 'selected', [selection.ids, selection.records]);
634 .delegate('td.oe-record-delete button', 'click', function (e) {
636 var $row = $(e.target).closest('tr');
637 $(self).trigger('deleted', [[self.row_id($row)]]);
639 .delegate('td.oe-field-cell button', 'click', function (e) {
641 var $target = $(e.currentTarget),
642 field = $target.closest('td').data('field'),
643 $row = $target.closest('tr'),
644 record_id = self.row_id($row),
645 index = self.row_position($row);
647 $(self).trigger('action', [field, record_id, function () {
648 return self.reload_record(index, true);
651 .delegate('tr', 'click', function (e) {
653 self.dataset.index = self.row_position(e.currentTarget);
657 row_clicked: function () {
660 [this.rows[this.dataset.index].data.id.value,
663 render: function () {
665 this.$current.remove();
667 this.$current = this.$_element.clone(true);
668 this.$current.empty().append(
669 QWeb.render('ListView.rows', _.extend({
670 render_cell: openerp.base.format_cell}, this)));
673 * Gets the ids of all currently selected records, if any
674 * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves.
676 get_selection: function () {
677 if (!this.options.selectable) {
680 var rows = this.rows;
681 var result = {ids: [], records: []};
682 this.$current.find('th.oe-record-selector input:checked')
683 .closest('tr').each(function () {
685 _(rows[$(this).data('index')].data).each(function (obj, key) {
686 record[key] = obj.value;
688 result.ids.push(record.id);
689 result.records.push(record);
694 * Returns the index of the row in the list of rows.
696 * @param {Object} row the selected row
697 * @returns {Number} the position of the row in this.rows
699 row_position: function (row) {
700 return $(row).data('index');
703 * Returns the identifier of the object displayed in the provided table
706 * @param {Object} row the selected table row
707 * @returns {Number|String} the identifier of the row's object
709 row_id: function (row) {
710 return this.rows[this.row_position(row)].data.id.value;
713 * Death signal, cleans up list
715 apoptosis: function () {
716 if (!this.$current) { return; }
717 this.$current.remove();
718 this.$current = null;
719 this.$_element.remove();
721 get_records: function () {
722 return _(this.rows).map(function (row) {
724 _(row.data).each(function (obj, key) {
725 record[key] = obj.value;
727 return {count: 1, values: record};
731 * Transforms a record from what is returned by a dataset read (a simple
732 * mapping of ``$fieldname: $value``) to the format expected by list rows
741 * This format allows for the insertion of a bunch of metadata (names,
744 * @param {Object} record original record, in dataset format
745 * @returns {Object} record displayable in a form or list view
747 transform_record: function (record) {
748 // TODO: colors handling
750 form_record = {data: form_data};
752 _(record).each(function (value, key) {
753 form_data[key] = {value: value};
759 * Reloads the record at index ``row_index`` in the list's rows.
761 * By default, simply re-renders the record. If the ``fetch`` parameter is
762 * provided and ``true``, will first fetch the record anew.
764 * @param {Number} record_index index of the record to reload
765 * @param {Boolean} fetch fetches the record from remote before reloading it
767 reload_record: function (record_index, fetch) {
771 // save index to restore it later, if already set
772 var old_index = this.dataset.index;
773 this.dataset.index = record_index;
774 read_p = this.dataset.read_index(
775 _.filter(_.pluck(this.columns, 'name'), _.identity),
777 var form_record = self.transform_record(record);
778 self.rows.splice(record_index, 1, form_record);
779 self.dataset.index = old_index;
784 return $.when(read_p).then(function () {
785 self.$current.children().eq(record_index)
786 .replaceWith(self.render_record(record_index)); });
789 * Renders a list record to HTML
791 * @param {Number} record_index index of the record to render in ``this.rows``
792 * @returns {String} QWeb rendering of the selected record
794 render_record: function (record_index) {
795 return QWeb.render('ListView.row', {
796 columns: this.columns,
797 options: this.options,
798 row: this.rows[record_index],
799 row_parity: (record_index % 2 === 0) ? 'even' : 'odd',
800 row_index: record_index,
801 render_cell: openerp.base.format_cell
805 * Stops displaying the records matching the provided ids.
807 * @param {Array} ids identifiers of the records to remove
809 drop_records: function (ids) {
812 .map(function (record, index) {
813 return {index: index, id: record.data.id.value};
814 }).filter(function (record) {
815 return _(ids).contains(record.id);
817 .each(function (record) {
818 self.$current.find('tr:eq(' + record.index + ')').remove();
819 self.rows.splice(record.index, 1);
824 openerp.base.ListView.Groups = openerp.base.Class.extend( /** @lends openerp.base.ListView.Groups# */{
825 passtrough_events: 'action deleted row_link',
827 * Grouped display for the ListView. Handles basic DOM events and interacts
828 * with the :js:class:`~openerp.base.DataGroup` bound to it.
830 * Provides events similar to those of
831 * :js:class:`~openerp.base.ListView.List`
833 init: function (view) {
835 this.options = view.options;
836 this.columns = view.columns;
837 this.datagroup = null;
844 make_fragment: function () {
845 return document.createDocumentFragment();
848 * Returns a DOM node after which a new tbody can be inserted, so that it
849 * follows the provided row.
851 * Necessary to insert the result of a new group or list view within an
852 * existing groups render, without losing track of the groups's own
855 * @param {HTMLTableRowElement} row the row after which the caller wants to insert a body
856 * @returns {HTMLTableSectionElement} element after which a tbody can be inserted
858 point_insertion: function (row) {
860 var red_letter_tboday = $row.closest('tbody')[0];
862 var $next_siblings = $row.nextAll();
863 if ($next_siblings.length) {
864 var $root_kanal = $('<tbody>').insertAfter(red_letter_tboday);
866 $root_kanal.append($next_siblings);
867 this.elements.splice(
868 _.indexOf(this.elements, red_letter_tboday),
872 return red_letter_tboday;
874 make_paginator: function () {
876 var $prev = $('<button type="button" data-pager-action="previous"><</button>')
877 .click(function (e) {
881 self.$row.closest('tbody').next()
882 .replaceWith(self.render());
884 var $next = $('<button type="button" data-pager-action="next">></button>')
885 .click(function (e) {
889 self.$row.closest('tbody').next()
890 .replaceWith(self.render());
892 this.$row.children().last()
894 .append('<span class="oe-pager-state"></span>')
897 open: function (point_insertion) {
898 this.render().insertAfter(point_insertion);
899 this.make_paginator();
902 this.$row.children().last().empty();
906 * Prefixes ``$node`` with floated spaces in order to indent it relative
907 * to its own left margin/baseline
909 * @param {jQuery} $node jQuery object to indent
910 * @param {Number} level current nesting level, >= 1
911 * @returns {jQuery} the indentation node created
913 indent: function ($node, level) {
915 .css({'float': 'left', 'white-space': 'pre'})
916 .text(new Array(level).join(' '))
919 render_groups: function (datagroups) {
921 var placeholder = this.make_fragment();
922 _(datagroups).each(function (group) {
923 if (self.children[group.value]) {
924 self.children[group.value].apoptosis();
925 delete self.children[group.value];
927 var child = self.children[group.value] = new openerp.base.ListView.Groups(self.view, {
928 options: self.options,
929 columns: self.columns
931 self.bind_child_events(child);
932 child.datagroup = group;
934 var $row = child.$row = $('<tr>');
935 if (group.openable) {
936 $row.click(function (e) {
937 if (!$row.data('open')) {
938 $row.data('open', true)
939 .find('span.ui-icon')
940 .removeClass('ui-icon-triangle-1-e')
941 .addClass('ui-icon-triangle-1-s');
942 child.open(self.point_insertion(e.currentTarget));
944 $row.removeData('open')
945 .find('span.ui-icon')
946 .removeClass('ui-icon-triangle-1-s')
947 .addClass('ui-icon-triangle-1-e');
952 placeholder.appendChild($row[0]);
954 var $group_column = $('<th class="oe-group-name">').appendTo($row);
955 // Don't fill this if group_by_no_leaf but no group_by
956 if (group.grouped_on) {
958 row_data[group.grouped_on] = group;
959 var group_column = _(self.columns).detect(function (column) {
960 return column.id === group.grouped_on; });
961 $group_column.html(openerp.base.format_cell(
962 row_data, group_column, "Undefined"
964 if (group.openable) {
965 // Make openable if not terminal group & group_by_no_leaf
967 .prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">');
970 self.indent($group_column, group.level);
972 $('<td>').text(group.length).appendTo($row);
974 if (self.options.selectable) {
977 _(self.columns).chain()
978 .filter(function (column) {return !column.invisible;})
979 .each(function (column) {
981 // do not do anything
982 } else if (column.id in group.aggregates) {
983 var value = group.aggregates[column.id];
985 if (column.type === 'integer') {
987 } else if (column.type === 'float') {
991 .text(_.sprintf(format, value))
997 if (self.options.deletable) {
998 $row.append('<td class="oe-group-pagination">');
1003 bind_child_events: function (child) {
1004 var $this = $(this),
1006 $(child).bind('selected', function (e) {
1007 // can have selections spanning multiple links
1008 var selection = self.get_selection();
1009 $this.trigger(e, [selection.ids, selection.records]);
1010 }).bind(this.passtrough_events, function (e) {
1011 // additional positional parameters are provided to trigger as an
1012 // Array, following the event type or event object, but are
1013 // provided to the .bind event handler as *args.
1014 // Convert our *args back into an Array in order to trigger them
1015 // on the group itself, so it can ultimately be forwarded wherever
1016 // it's supposed to go.
1017 var args = Array.prototype.slice.call(arguments, 1);
1018 $this.trigger.call($this, e, args);
1021 render_dataset: function (dataset) {
1024 list = new openerp.base.ListView.List(this, {
1025 options: this.options,
1026 columns: this.columns,
1030 this.bind_child_events(list);
1032 var view = this.view,
1033 limit = view.limit(),
1034 d = new $.Deferred(),
1035 page = this.datagroup.openable ? this.page : view.page;
1037 dataset.read_slice({
1038 fields: _.filter(_.pluck(_.select(this.columns, function(x) {return x.tag == "field";}), 'name'), _.identity),
1039 offset: page * limit,
1041 }, function (records) {
1042 if (!self.datagroup.openable) {
1043 view.configure_pager(dataset);
1045 var pages = Math.ceil(dataset.ids.length / limit);
1047 .find('.oe-pager-state')
1048 .text(_.sprintf('%d/%d', page + 1, pages))
1050 .find('button[data-pager-action=previous]')
1051 .attr('disabled', page === 0)
1053 .find('button[data-pager-action=next]')
1054 .attr('disabled', page === pages - 1);
1057 var form_records = _(records).map(
1058 $.proxy(list, 'transform_record'));
1060 rows.splice(0, rows.length);
1061 rows.push.apply(rows, form_records);
1067 setup_resequence_rows: function (list, dataset) {
1068 // drag and drop enabled if list is not sorted and there is a
1069 // "sequence" column in the view.
1070 if ((dataset.sort && dataset.sort())
1071 || !_(this.columns).any(function (column) {
1072 return column.name === 'sequence'; })) {
1075 // ondrop, move relevant record & fix sequences
1076 list.$current.sortable({
1077 stop: function (event, ui) {
1078 var from = ui.item.data('index'),
1079 to = ui.item.prev().data('index') || 0;
1080 if (from === to) { return; }
1081 list.rows.splice(to, 0, list.rows.splice(from, 1)[0]);
1083 ui.item.parent().children().each(function (i, e) {
1084 // reset record-index accelerators on rows and even/odd
1085 var even = i%2 === 0;
1086 $(e).data('index', i)
1087 .toggleClass('even', even)
1088 .toggleClass('odd', !even);
1091 // resequencing time!
1092 var data, index = to,
1093 // if drag to 1st row (to = 0), start sequencing from 0
1094 // (exclusive lower bound)
1095 seq = to ? list.rows[to - 1].data.sequence.value : 0;
1096 while (++seq, list.rows[index]) {
1097 data = list.rows[index].data;
1098 data.sequence.value = seq;
1099 // write are independent from one another, so we can just
1100 // launch them all at the same time and we don't really
1101 // give a fig about when they're done
1102 dataset.write(data.id.value, {sequence: seq});
1103 list.reload_record(index++);
1108 render: function (post_render) {
1110 var $element = $('<tbody>');
1111 this.elements = [$element[0]];
1113 this.datagroup.list(
1114 _(this.view.visible_columns).chain()
1115 .filter(function (column) { return column.tag === 'field' })
1116 .pluck('name').value(),
1118 $element[0].appendChild(
1119 self.render_groups(groups));
1120 if (post_render) { post_render(); }
1121 }, function (dataset) {
1122 self.render_dataset(dataset).then(function (list) {
1123 self.children[null] = list;
1125 [list.$current.replaceAll($element)[0]];
1126 self.setup_resequence_rows(list, dataset);
1127 if (post_render) { post_render(); }
1133 * Returns the ids of all selected records for this group, and the records
1136 get_selection: function () {
1137 var ids = [], records = [];
1140 .each(function (child) {
1141 var selection = child.get_selection();
1142 ids.push.apply(ids, selection.ids);
1143 records.push.apply(records, selection.records);
1146 return {ids: ids, records: records};
1148 apoptosis: function () {
1149 _(this.children).each(function (child) {
1153 $(this.elements).remove();
1156 get_records: function () {
1157 if (_(this.children).isEmpty()) {
1159 count: this.datagroup.length,
1160 values: this.datagroup.aggregates
1163 return _(this.children).chain()
1164 .map(function (child) {
1165 return child.get_records();
1166 }).flatten().value();
1169 * Stops displaying the records with the linked ids, assumes these records
1170 * were deleted from the DB.
1172 * This is the up-signal from the `deleted` event on groups and lists.
1174 * @param {Array} ids list of identifier of the records to remove.
1176 drop_records: function (ids) {
1177 _.each(this.children, function (child) {
1178 child.drop_records(ids);
1184 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: