diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index 3462efd656e..70feda44bc0 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -585,56 +585,6 @@ class ListView(View): view_attributes['editable'] = 'bottom' return view - @openerpweb.jsonrequest - def fill(self, request, model, id, domain, - offset=0, limit=False, sort=None): - return self.do_fill(request, model, id, domain, offset, limit, sort) - - def do_fill(self, request, model, id, domain, - offset=0, limit=False, sort=None): - """ Returns all information needed to fill a table: - - * view with processed ``editable`` flag - * fields (columns) with processed ``invisible`` flag - * rows with processed ``attrs`` and ``colors`` - - .. note:: context is passed through ``request`` parameter - - :param request: OpenERP request - :type request: openerpweb.openerpweb.JsonRequest - :type str model: OpenERP model for this list view - :type int id: view_id, or False if none provided - :param list domain: the search domain to search for - :param int offset: search offset, for pagination - :param int limit: search limit, for pagination - :returns: hell if I have any idea yet - """ - view = self.fields_view_get(request, model, id, toolbar=True) - - rows = DataSet().do_search_read(request, model, - offset=offset, limit=limit, - domain=domain, sort=sort) - eval_context = request.session.evaluation_context( - request.context) - - if sort: - sort_criteria = sort.split(',')[0].split(' ') - view['sorted'] = { - 'field': sort_criteria[0], - 'reversed': sort_criteria[1] == 'DESC' - } - else: - view['sorted'] = {} - return { - 'view': view, - 'records': [ - {'data': dict((key, {'value': value}) - for key, value in row.iteritems()), - 'color': self.process_colors(view, row, eval_context)} - for row in rows - ] - } - def process_colors(self, view, row, context): colors = view['arch']['attrs'].get('colors') diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 38fbfb115ef..0ab68967db9 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -552,6 +552,17 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, text-align: right; } +.openerp .oe-listview tfoot td { + padding: 3px 3px 0; +} +.openerp .oe-listview .oe-list-footer { + text-align: center; + white-space: nowrap; +} +.openerp .oe-listview .oe-list-footer span { + margin: 0 1em; +} + /** list rounded corners rounded corners are a pain on tables: need to round not only table, but @@ -572,11 +583,13 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%, -moz-border-radius-topright: 7px; border-top-right-radius: 7px; } +.openerp .oe-listview table tfoot td:first-child, .openerp .oe-listview table tbody:last-child tr:last-child th:first-child { -webkit-border-bottom-left-radius: 7px; -moz-border-radius-bottomleft: 7px; border-bottom-left-radius: 7px; } +.openerp .oe-listview table tfoot td:last-child, .openerp .oe-listview table tbody:last-child tr:last-child td:last-child { -webkit-border-bottom-right-radius: 7px; -moz-border-radius-bottomright: 7px; diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index 1d2fab9b9bc..60bffc8bff3 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -104,7 +104,7 @@ openerp.base.ContainerDataGroup = openerp.base.DataGroup.extend( || key === field_name + '_count') { return; } - aggregates[key] = value; + aggregates[key] = value || 0; }); return { diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 89f3a91f676..3374050fa17 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -1080,7 +1080,7 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({ }, check_load: function() { if(this.is_started && this.is_setted) { - this.list_view.do_reload(); + this.list_view.reload_view(); } } }); @@ -1090,11 +1090,11 @@ openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ this.dataset.ids = _.without.apply(null, [this.dataset.ids].concat(ids)); this.dataset.count = this.dataset.ids.length; // there may be a faster way - this.do_reload(); + this.reload_view(); this.m2m_field.on_ui_change(); }, - do_reload: function () { + reload_view: function () { /* Dear xmo, according to your comments, this method's implementation in list view seems * to be a little bit bullshit. * I assume the list view will be changed later, so I hope it will support static datasets. @@ -1115,7 +1115,7 @@ openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({ if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) { self.dataset.ids.push(element_id); self.dataset.count = self.dataset.ids.length; - self.do_reload(); + self.reload_view(); } pop.stop(); }); @@ -1188,7 +1188,7 @@ openerp.base.form.Many2XSelectPopup = openerp.base.BaseWidget.extend({ var tmphack = {"loaded": false}; self.view_list.on_loaded.add_last(function() { if ( !tmphack.loaded ) { - self.view_list.do_reload(); + self.view_list.reload_view(); tmphack.loaded = true; }; }); diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 77a66731f34..3a482164bb6 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -44,7 +44,6 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @borrows openerp.base.ActionExecutor#execute_action as #execute_action */ init: function(view_manager, session, element_id, dataset, view_id, options) { - var self = this; this._super(session, element_id); this.view_manager = view_manager || new openerp.base.NullViewManager(); this.dataset = dataset; @@ -56,41 +55,35 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.options = _.extend({}, this.defaults, options || {}); this.flags = this.view_manager.action.flags; - this.groups = new openerp.base.ListView.Groups(this, { - options: this.options, - columns: this.columns - }); + this.set_groups(new openerp.base.ListView.Groups(this)); + }, + /** + * Set a custom Group construct as the root of the List View. + * + * @param {openerp.base.ListView.Groups} groups + */ + set_groups: function (groups) { + var self = this; + if (this.groups) { + $(this.groups).unbind("selected deleted action row_link"); + delete this.groups; + } + + this.groups = groups; $(this.groups).bind({ - 'selected': function (e, selection) { - self.$element.find('#oe-list-delete') - .toggle(!!selection.length); + 'selected': function (e, ids, records) { + self.do_select(ids, records); }, 'deleted': function (e, ids) { self.do_delete(ids); }, 'action': function (e, action_name, id, callback) { - var action = _.detect(self.columns, function (field) { - return field.name === action_name; - }); - if (!action) { return; } - self.execute_action( - action, self.dataset, self.session.action_manager, - id, function () { - if (callback) { - callback(); - } - }); + self.do_action(action_name, id, callback); }, 'row_link': function (e, index, id, dataset) { - _.extend(self.dataset, { - domain: dataset.domain, - context: dataset.context - }).read_slice([], null, null, function () { - self.select_record(index); - }); + self.do_activate_record(index, id, dataset); } }); - }, /** * View startup method, the default behavior is to set the ``oe-listview`` @@ -100,11 +93,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi */ start: function() { this.$element.addClass('oe-listview'); - return this.rpc("/base/listview/load", { - model: this.model, - view_id: this.view_id, - toolbar: !!this.flags.sidebar - }, this.on_loaded); + return this.reload_view(); }, /** * Called after loading the list view's description, sets up such things @@ -127,15 +116,15 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * @param {Object} data.fields_view fields_view_get result (processed) * @param {Object} data.fields_view.fields mapping of fields for the current model * @param {Object} data.fields_view.arch current list view descriptor - * @param {Array} columns columns to move to the front (and make visible) + * @param {Boolean} grouped Is the list view grouped */ - on_loaded: function(data, columns) { + on_loaded: function(data, grouped) { var self = this; this.fields_view = data.fields_view; //this.log(this.fields_view); this.name = "" + this.fields_view.arch.attrs.string; - this.setup_columns(this.fields_view.fields, columns); + this.setup_columns(this.fields_view.fields, grouped); if (!this.fields_view.sorted) { this.fields_view.sorted = {}; } @@ -152,7 +141,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi self.dataset.sort($(this).data('id')); // TODO: should only reload content (and set the right column to a sorted display state) - self.do_reload(); + self.reload_view(); }); this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar); @@ -163,9 +152,9 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * visible. * * @param {Object} fields fields_view_get's fields section - * @param {Array} groupby_columns columns the ListView is grouped by + * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed */ - setup_columns: function (fields, groupby_columns) { + setup_columns: function (fields, grouped) { var domain_computer = openerp.base.form.compute_domain; var noop = function () { return {}; }; @@ -193,7 +182,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.columns.push.apply( this.columns, _(this.fields_view.arch.children).map(field_to_column)); - if (groupby_columns) { + if (grouped) { this.columns.unshift({ id: '_group', tag: '', string: "Group", meta: true, attrs_for: function () { return {}; } @@ -206,6 +195,19 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.visible_columns = _.filter(this.columns, function (column) { return column.invisible !== '1'; }); + + this.aggregate_columns = _(this.columns).chain() + .filter(function (column) { + return column['sum'] || column['avg'];}) + .map(function (column) { + var func = column['sum'] ? 'sum' : 'avg'; + return { + field: column.id, + type: column.type, + 'function': func, + label: column[func] + }; + }).value(); }, /** * Used to handle a click on a table row, if no other handler caught the @@ -245,26 +247,20 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi this.hidden = true; }, /** - * Reloads the search view based on the current settings (dataset & al) + * Reloads the list view based on the current settings (dataset & al) * - * @param {Array} [primary_columns] columns to bring to the front of the - * sequence + * @param {Boolean} [grouped] Should the list be displayed grouped */ - do_reload: function (primary_columns) { - // TODO: should just fields_view_get I think + reload_view: function (grouped) { var self = this; this.dataset.offset = 0; this.dataset.limit = false; - return this.rpc('/base/listview/fill', { - 'model': this.dataset.model, - 'id': this.view_id, - 'context': this.dataset.context, - 'domain': this.dataset.domain, - 'sort': this.dataset.sort && this.dataset.sort() - }, function (result) { - if (result.view) { - self.on_loaded({fields_view: result.view}, primary_columns); - } + return this.rpc('/base/listview/load', { + model: this.model, + view_id: this.view_id, + toolbar: !!this.flags.sidebar + }, function (field_view_get) { + self.on_loaded(field_view_get, grouped); }); }, /** @@ -286,16 +282,17 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi self.dataset.context = results.context; self.dataset.domain = results.domain; self.groups.datagroup = new openerp.base.DataGroup( - self.session, self.dataset.model, + self.session, self.model, results.domain, results.context, results.group_by); if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) { results.group_by = null; } - self.do_reload(results.group_by).then(function () { - self.$element.find('table').append(self.groups.render()); - }); + self.reload_view(!!results.group_by).then(function () { + self.$element.find('table').append( + self.groups.render(function () { + self.compute_aggregates();}));}); }); }, /** @@ -330,6 +327,58 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi // TODO only refresh modified rows }); }, + /** + * Handles the signal indicating that a new record has been selected + * + * @param {Array} ids selected record ids + * @param {Array} records selected record values + */ + do_select: function (ids, records) { + this.$element.find('#oe-list-delete') + .toggle(!!ids.length); + + if (!records.length) { + this.compute_aggregates(); + return; + } + this.compute_aggregates(records); + }, + /** + * Handles action button signals on a record + * + * @param {String} name action name + * @param {Object} id id of the record the action should be called on + * @param {Function} callback should be called after the action is executed, if non-null + */ + do_action: function (name, id, callback) { + var action = _.detect(this.columns, function (field) { + return field.name === name; + }); + if (!action) { return; } + this.execute_action( + action, this.dataset, this.session.action_manager, + id, function () { + if (callback) { + callback(); + } + }); + }, + /** + * Handles the activation of a record (clicking on it) + * + * @param {Number} index index of the record in the dataset + * @param {Object} id identifier of the activated record + * @param {openobject.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups) + */ + do_activate_record: function (index, id, dataset) { + var self = this; + _.extend(this.dataset, { + domain: dataset.domain, + context: dataset.context + }).read_slice([], 0, false, function () { + self.select_record(index); + }); + }, /** * Handles signal for the addition of a new record (can be a creation, * can be the addition from a remote source, ...) @@ -337,14 +386,96 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * The default implementation is to switch to a new record on the form view */ do_add_record: function () { - this.notification.notify('Add', "New record"); this.select_record(null); }, /** * Handles deletion of all selected lines */ do_delete_selected: function () { - this.do_delete(this.groups.get_selection()); + this.do_delete(this.groups.get_selection().ids); + }, + /** + * Computes the aggregates for the current list view, either on the + * records provided or on the records of the internal + * :js:class:`~openerp.base.ListView.Group`, by calling + * :js:func:`~openerp.base.ListView.group.get_records`. + * + * Then displays the aggregates in the table through + * :js:method:`~openerp.base.ListView.display_aggregates`. + * + * @param {Array} [records] + */ + compute_aggregates: function (records) { + if (_.isEmpty(this.aggregate_columns)) { + return; + } + if (_.isEmpty(records)) { + records = this.groups.get_records(); + } + + var aggregator = this.build_aggregator(this.aggregate_columns); + this.display_aggregates( + _(records).reduce(aggregator, aggregator).value()); + }, + /** + * Creates a stateful callable aggregator object, which can be reduced over + * a collection of records in order to build the aggregations described + * by the parameter + * + * @param {Array} aggregation_descriptors + */ + build_aggregator: function (aggregation_descriptors) { + var values = {}; + var descriptors = {}; + _(aggregation_descriptors).each(function (descriptor) { + values[descriptor.field] = []; + descriptors[descriptor.field] = descriptor; + }); + + var aggregator = function (_i, record) { + _(values).each(function (collection, key) { + collection.push(record[key]); + }); + + return aggregator; + }; + aggregator.value = function () { + var result = {}; + + _(values).each(function (collection, key) { + var value; + switch(descriptors[key]['function']) { + case 'avg': + value = (_(collection).chain() + .filter(function (item) { + return !_.isUndefined(item); }) + .reduce(function (total, item) { + return total + item; }, 0).value() + / collection.length); + break; + case 'sum': + value = (_(collection).chain() + .filter(function (item) { + return !_.isUndefined(item); }) + .reduce(function (total, item) { + return total + item; }, 0).value()); + break; + } + result[key] = value; + }); + + return result; + }; + return aggregator; + }, + display_aggregates: function (aggregation) { + var $footer = this.$element.find('.oe-list-footer').empty(); + _(this.aggregate_columns).each(function (column) { + $(_.sprintf( + "%s: %.2f", + column.label, aggregation[column.field])) + .appendTo($footer); + }); } // TODO: implement reorder (drag and drop rows) }); @@ -378,7 +509,6 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List */ init: function (opts) { var self = this; - // columns, rows, options this.options = opts.options; this.columns = opts.columns; @@ -389,7 +519,9 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List .appendTo(document.body) .delegate('th.oe-record-selector', 'click', function (e) { e.stopPropagation(); - $(self).trigger('selected', [self.get_selection()]); + var selection = self.get_selection(); + $(self).trigger( + 'selected', [selection.ids, selection.records]); }) .delegate('td.oe-record-delete button', 'click', function (e) { e.stopPropagation(); @@ -422,17 +554,24 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List }, /** * Gets the ids of all currently selected records, if any - * @returns {Array} empty if no record is selected (or the list view is not selectable) + * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves. */ get_selection: function () { if (!this.options.selectable) { return []; } var rows = this.rows; - return this.$current.find('th.oe-record-selector input:checked') - .closest('tr').map(function () { - return rows[$(this).prevAll().length].data.id.value; - }).get(); + var result = {ids: [], records: []}; + this.$current.find('th.oe-record-selector input:checked') + .closest('tr').each(function () { + var record = {}; + _(rows[$(this).prevAll().length].data).each(function (obj, key) { + record[key] = obj.value; + }); + result.ids.push(record.id); + result.records.push(record); + }); + return result; }, /** * Returns the index of the row in the list of rows. @@ -460,6 +599,16 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List if (!this.$current) { return; } this.$current.remove(); this.$current = null; + this.$_element.remove(); + }, + get_records: function () { + return _(this.rows).map(function (row) { + var record = {}; + _(row.data).each(function (obj, key) { + record[key] = obj.value; + }); + return record; + }); } // drag and drop // editable? @@ -472,11 +621,11 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr * Provides events similar to those of * :js:class:`~openerp.base.ListView.List` */ - init: function (view, opts) { + init: function (view) { this.view = view; - this.options = opts.options; - this.columns = opts.columns; - this.datagroup = {}; + this.options = view.options; + this.columns = view.columns; + this.datagroup = null; this.sections = []; this.children = {}; @@ -502,19 +651,19 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr */ point_insertion: function (row) { var $row = $(row); - var red_letter_tbody = $row.closest('tbody')[0]; + var red_letter_tboday = $row.closest('tbody')[0]; var $next_siblings = $row.nextAll(); if ($next_siblings.length) { - var $root_kanal = $('').insertAfter(red_letter_tbody); + var $root_kanal = $('').insertAfter(red_letter_tboday); $root_kanal.append($next_siblings); this.elements.splice( - _.indexOf(this.elements, red_letter_tbody), + _.indexOf(this.elements, red_letter_tboday), 0, $root_kanal[0]); } - return red_letter_tbody; + return red_letter_tboday; }, open_group: function (e, group) { var row = e.currentTarget; @@ -570,7 +719,6 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr } placeholder.appendChild($row[0]); - var $group_column = $('').appendTo($row); if (group.grouped_on) { // Don't fill this if group_by_no_leaf but no group_by @@ -615,7 +763,8 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr self = this; $(child).bind('selected', function (e) { // can have selections spanning multiple links - $this.trigger(e, [self.get_selection()]); + var selection = self.get_selection(); + $this.trigger(e, [selection.ids, selection.records]); }).bind('action', function (e, name, id, callback) { if (!callback) { callback = function () { @@ -652,46 +801,62 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr this.bind_child_events(list); var d = new $.Deferred(); - this.view.rpc('/base/listview/fill', { - model: dataset.model, - id: this.view.view_id, - context: dataset.context, - domain: dataset.domain, - sort: dataset.sort && dataset.sort() - }, function (result) { - rows.splice(0, rows.length); - rows.push.apply(rows, result.records); - list.render(); - d.resolve(list); - }); + dataset.read_slice( + _.filter(_.pluck(this.columns, 'name'), _.identity), + 0, false, + function (records) { + var form_records = _(records).map(function (record) { + // TODO: colors handling + var form_data = {}, + form_record = {data: form_data}; + + _(record).each(function (value, key) { + form_data[key] = {value: value}; + }); + + return form_record; + }); + + rows.splice(0, rows.length); + rows.push.apply(rows, form_records); + list.render(); + d.resolve(list); + }); return d.promise(); }, - render: function () { + render: function (post_render) { var self = this; var $element = $(''); this.elements = [$element[0]]; this.datagroup.list(function (groups) { $element[0].appendChild( self.render_groups(groups)); + if (post_render) { post_render(); } }, function (dataset) { self.render_dataset(dataset).then(function (list) { self.children[null] = list; self.elements = [list.$current.replaceAll($element)[0]]; + if (post_render) { post_render(); } }); }); return $element; }, /** - * Returns the ids of all selected records for this group + * Returns the ids of all selected records for this group, and the records + * themselves */ get_selection: function () { - return _(this.children).chain() - .map(function (child) { - return child.get_selection(); - }) - .flatten() - .value(); + var ids = [], records = []; + + _(this.children) + .each(function (child) { + var selection = child.get_selection(); + ids.push.apply(ids, selection.ids); + records.push.apply(records, selection.records); + }); + + return {ids: ids, records: records}; }, apoptosis: function () { _(this.children).each(function (child) { @@ -699,6 +864,12 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr }); $(this.elements).remove(); return this; + }, + get_records: function () { + return _(this.children).chain() + .map(function (child) { + return child.get_records(); + }).flatten().value(); } }); }; diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index b0fbe57e0f1..a83c38cc006 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -357,7 +357,7 @@ openerp.base.View = openerp.base.Controller.extend({ * @param {Object} [action_data.context=null] additional action context, to add to the current context * @param {openerp.base.DataSet} dataset a dataset object used to communicate with the server * @param {openerp.base.ActionManager} action_manager object able to actually execute the action, if any is fetched - * @param {Number} [record_id] the identifier of the object on which the action is to be applied + * @param {Object} [record_id] the identifier of the object on which the action is to be applied * @param {Function} on_no_action callback to execute if the action does not generate any result (no new action) */ execute_action: function (action_data, dataset, action_manager, record_id, on_no_action) { diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index a240cecf24f..2ae67716a98 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -156,7 +156,7 @@ - + @@ -165,7 +165,7 @@ @@ -205,6 +205,13 @@ + + + + + + + diff --git a/doc/source/development.rst b/doc/source/development.rst index 514e094b689..bfb176cb5c5 100644 --- a/doc/source/development.rst +++ b/doc/source/development.rst @@ -281,6 +281,105 @@ abstract types, used to implement input widgets: .. TODO: insert Input, Field, Filter, and just about every Field subclass +List View ++++++++++ + +OpenERP Web's list views don't actually exist as such in OpenERP itself: a +list view is an OpenERP tree view in the ``view_mode`` form. + +The overall purpose of a list view is to display collections of objects in two +main forms: per-object, where each object is a row in and of itself, and +grouped, where multiple objects are represented with a single row providing +an aggregated view of all grouped objects. + +These two forms can be mixed within a single list view, if needed. + +The root of a list view is :js:class:`openerp.base.ListView`, which may need +to be overridden (partially or fully) to control list behavior in non-view +cases (when using a list view as sub-component of a form widget for instance). + +Creation and Initialization +""""""""""""""""""""""""""" + +As with most OpenERP Web views, the list view's +:js:func:`~openerp.base.ListView.init` takes quite a number of arguments. + +While most of them are the standard view constructor arguments +(``view_manager``, ``session``, ``element_id``, ``dataset`` and an +optional ``view_id``), the list view adds a number of options for basic +customization (without having to override methods or templates): + +``selectable`` (default: ``true``) + Indicates that the list view should allow records to be selected + individually. Displays selection check boxes to the left of all record rows, + and allows for the triggering of the + :ref:`selection event `. +``deletable`` (default: ``true``) + Indicates that the list view should allow records to be removed + individually. Displays a deletion button to the right of all record rows, + and allows for the triggering of the + :ref:`deletion event `. +``header`` (default: ``true``) + Indicates that list columns should bear a header sporting their name (for + non-action columns). +``addable`` (default: ``"New"``) + Indicates that a record addition/creation button should be displayed in + the list's header, along with its label. Also allows for the triggering of + the :ref:`record addition event `. +``sortable`` (default: ``true``) + Indicates that the list view can be sorted per-column (by clicking on its + column headers). + + .. TODO: event? +``reorderable`` (default: ``true``) + Indicates that the list view records can be reordered (and re-sequenced) + by drag and drop. + + .. TODO: event? + +Events +"""""" +.. _listview-events-addition: + +Addition +'''''''' +The addition event is used to add a record to an existing list view. The +default behavior is to switch to the form view, on a new record. + +Addition behavior can be overridden by replacing the +:js:func:`~openerp.base.ListView.do_add_record` method. + +.. _listview-events-selection: + +Selection +''''''''' +The selection event is triggered when a given record is selected in the list +view. + +It can be overridden by replacing the +:js:func:`~openerp.base.ListView.do_select` method. + +The default behavior is simply to hide or display the list-wise deletion button +depending on whether there are selected records or not. + +.. _listview-events-deletion: + +Deletion +'''''''' +The deletion event is triggered when the user tries to remove 1..n records from +the list view, either individually or globally (via the header button). + +Deletion can be overridden by replacing the +:js:func:`~openerp.base.ListView.do_delete` method. By default, this method +calls :js:func:`~openerp.base.DataSet.unlink` in order to remove the records +entirely. + +.. note:: the list-wise deletion button (next to the record addition button) + simply proxies to :js:func:`~openerp.base.ListView.do_delete` after + obtaining all selected record ids, but it is possible to override it + alone by replacing + :js:func:`~openerp.base.ListView.do_delete_selected`. + Internal API Doc ----------------