openerp.web.list = function (instance) {
var _t = instance.web._t,
- _lt = instance.web._lt;
+ _lt = instance.web._lt;
var QWeb = instance.web.qweb;
instance.web.views.add('list', 'instance.web.ListView');
instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ {
// whether the view rows can be reordered (via vertical drag & drop)
'reorderable': true,
'action_buttons': true,
- // if true, the 'Import', 'Export', etc... buttons will be shown
- 'import_enabled': true,
+ //whether the editable property of the view has to be disabled
+ 'disable_editable_mode': false,
+ },
+ view_type: 'tree',
+ events: {
+ 'click thead th.oe_sortable[data-id]': 'sort_by_column'
},
/**
* Core class for list-type displays.
this.set_groups(new (this.options.GroupsType)(this));
if (this.dataset instanceof instance.web.DataSetStatic) {
- this.groups.datagroup = new instance.web.StaticDataGroup(this.dataset);
+ this.groups.datagroup = new StaticDataGroup(this.dataset);
} else {
- this.groups.datagroup = new instance.web.DataGroup(
+ this.groups.datagroup = new DataGroup(
this, this.model,
dataset.get_domain(),
dataset.get_context());
});
this.no_leaf = false;
+ this.grouped = false;
+ },
+ view_loading: function(r) {
+ return this.load_list(r);
},
set_default_options: function (options) {
this._super(options);
* @returns {$.Deferred} loading promise
*/
start: function() {
- this.$element.addClass('oe_list');
- return this.reload_view(null, null, true);
+ this.$el.addClass('oe_list');
+ return this._super();
},
/**
* Returns the style for the provided record in the current view (from the
* @param {Object} data.fields_view.arch current list view descriptor
* @param {Boolean} grouped Is the list view grouped
*/
- on_loaded: function(data, grouped) {
+ load_list: function(data) {
var self = this;
this.fields_view = data;
this.name = "" + this.fields_view.arch.attrs.string;
}).value();
}
- this.setup_columns(this.fields_view.fields, grouped);
+ this.setup_columns(this.fields_view.fields, this.grouped);
+
+ this.$el.html(QWeb.render(this._template, this));
+ this.$el.addClass(this.fields_view.arch.attrs['class']);
- this.$element.html(QWeb.render(this._template, this));
- this.$element.addClass(this.fields_view.arch.attrs['class']);
// Head hook
// Selecting records
- this.$element.find('.oe_list_record_selector').click(function(){
- self.$element.find('.oe_list_record_selector input').prop('checked',
- self.$element.find('.oe_list_record_selector').prop('checked') || false);
+ this.$el.find('.oe_list_record_selector').click(function(){
+ self.$el.find('.oe_list_record_selector input').prop('checked',
+ self.$el.find('.oe_list_record_selector').prop('checked') || false);
var selection = self.groups.get_selection();
$(self.groups).trigger(
'selected', [selection.ids, selection.records]);
});
- // Sorting columns
- this.$element.find('thead').delegate('th.oe_sortable[data-id]', 'click', function (e) {
- e.stopPropagation();
- var $this = $(this);
- self.dataset.sort($this.data('id'));
- if($this.hasClass("sortdown") || $this.hasClass("sortup")) {
- $this.toggleClass("sortdown").toggleClass("sortup");
- } else {
- $this.toggleClass("sortdown");
- }
- $this.siblings('.oe_sortable').removeClass("sortup sortdown");
-
- self.reload_content();
- });
-
- // Add button and Import link
+ // Add button
if (!this.$buttons) {
this.$buttons = $(QWeb.render("ListView.buttons", {'widget':self}));
if (this.options.$buttons) {
this.$buttons.appendTo(this.options.$buttons);
} else {
- this.$element.find('.oe_list_buttons').replaceWith(this.$buttons);
+ this.$el.find('.oe_list_buttons').replaceWith(this.$buttons);
}
this.$buttons.find('.oe_list_add')
.click(this.proxy('do_add_record'))
- .prop('disabled', grouped);
- this.$buttons.on('click', '.oe_list_button_import', function() {
- self.on_sidebar_import();
- return false;
- });
+ .prop('disabled', this.grouped);
}
// Pager
if (this.options.$buttons) {
this.$pager.appendTo(this.options.$pager);
} else {
- this.$element.find('.oe_list_pager').replaceWith(this.$pager);
+ this.$el.find('.oe_list_pager').replaceWith(this.$pager);
}
this.$pager
if (!this.sidebar && this.options.$sidebar) {
this.sidebar = new instance.web.Sidebar(this);
this.sidebar.appendTo(this.options.$sidebar);
- this.sidebar.add_items('other', [
- { label: _t("Import"), callback: this.on_sidebar_import },
+ this.sidebar.add_items('other', _.compact([
{ label: _t("Export"), callback: this.on_sidebar_export },
- { label: _t('Delete'), callback: this.do_delete_selected }
- ]);
+ self.is_action_enabled('delete') && { label: _t('Delete'), callback: this.do_delete_selected }
+ ]));
this.sidebar.add_toolbar(this.fields_view.toolbar);
- this.sidebar.$element.hide();
+ this.sidebar.$el.hide();
+ }
+ //Sort
+ if(this.dataset._sort.length){
+ if(this.dataset._sort[0].indexOf('-') == -1){
+ this.$el.find('th[data-id=' + this.dataset._sort[0] + ']').addClass("sortdown");
+ }else {
+ this.$el.find('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("sortup");
+ }
}
+ this.trigger('list_view_loaded', data, this.grouped);
+ },
+ sort_by_column: function (e) {
+ e.stopPropagation();
+ var $column = $(e.currentTarget);
+ this.dataset.sort($column.data('id'));
+ if($column.hasClass("sortdown") || $column.hasClass("sortup")) {
+ $column.toggleClass("sortup sortdown");
+ } else {
+ $column.addClass("sortdown");
+ }
+ $column.siblings('.oe_sortable').removeClass("sortup sortdown");
+
+ this.reload_content();
},
/**
* Configures the ListView pager based on the provided dataset's information
var total = dataset.size();
var limit = this.limit() || total;
- this.$pager.toggle(total !== 0);
+ if (total == 0)
+ this.$pager.hide();
+ else
+ this.$pager.css("display", "");
this.$pager.toggleClass('oe_list_pager_single_page', (total <= limit));
var spager = '-';
if (total) {
if (range_stop > total) {
range_stop = total;
}
- spager = _.str.sprintf('%d-%d of %d', range_start, range_stop, total);
+ spager = _.str.sprintf(_t("%d-%d of %d"), range_start, range_stop, total);
}
this.$pager.find('.oe_list_pager_state').text(spager);
* @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
*/
setup_columns: function (fields, grouped) {
- var domain_computer = instance.web.form.compute_domain;
-
- var noop = function () { return {}; };
- var field_to_column = function (field) {
- var name = field.attrs.name;
- var column = _.extend({id: name, tag: field.tag},
- fields[name], field.attrs);
- // modifiers computer
- if (column.modifiers) {
- var modifiers = JSON.parse(column.modifiers);
- column.modifiers_for = function (fields) {
- var out = {};
-
- for (var attr in modifiers) {
- if (!modifiers.hasOwnProperty(attr)) { continue; }
- var modifier = modifiers[attr];
- out[attr] = _.isBoolean(modifier)
- ? modifier
- : domain_computer(modifier, fields);
- }
-
- return out;
- };
- if (modifiers['tree_invisible']) {
- column.invisible = '1';
- } else {
- delete column.invisible;
- }
- column.modifiers = modifiers;
- } else {
- column.modifiers_for = noop;
- column.modifiers = {};
- }
- return column;
- };
-
+ var registry = instance.web.list.columns;
this.columns.splice(0, this.columns.length);
- this.columns.push.apply(
- this.columns,
- _(this.fields_view.arch.children).map(field_to_column));
+ this.columns.push.apply(this.columns,
+ _(this.fields_view.arch.children).map(function (field) {
+ var id = field.attrs.name;
+ return registry.for_(id, fields[id], field);
+ }));
if (grouped) {
- this.columns.unshift({
- id: '_group', tag: '', string: _t("Group"), meta: true,
- modifiers_for: function () { return {}; },
- modifiers: {}
- }, {
- id: '_count', tag: '', string: '#', meta: true,
- modifiers_for: function () { return {}; },
- modifiers: {}
- });
+ this.columns.unshift(
+ new instance.web.list.MetaColumn('_group', _t("Group")));
}
this.visible_columns = _.filter(this.columns, function (column) {
return column.invisible !== '1';
});
- this.aggregate_columns = _(this.visible_columns)
- .map(function (column) {
- if (column.type !== 'integer' && column.type !== 'float') {
- return {};
- }
- var aggregation_func = column['group_operator'] || 'sum';
- if (!(aggregation_func in column)) {
- return {};
- }
-
- return _.extend({}, column, {
- 'function': aggregation_func,
- label: column[aggregation_func]
- });
- });
+ this.aggregate_columns = _(this.visible_columns).invoke('to_aggregate');
},
/**
* Used to handle a click on a table row, if no other handler caught the
},
do_hide: function () {
if (this.sidebar) {
- this.sidebar.$element.hide();
+ this.sidebar.$el.hide();
}
if (this.$buttons) {
this.$buttons.hide();
/**
* Reloads the list view based on the current settings (dataset & al)
*
+ * @deprecated
* @param {Boolean} [grouped] Should the list be displayed grouped
* @param {Object} [context] context to send the server while loading the view
*/
reload_view: function (grouped, context, initial) {
- var self = this;
- var callback = function (field_view_get) {
- self.on_loaded(field_view_get, grouped);
- };
- if (this.embedded_view) {
- return $.Deferred().then(callback).resolve(this.embedded_view);
- } else {
- return this.rpc('/web/listview/load', {
- model: this.model,
- view_id: this.view_id,
- view_type: "tree",
- context: this.dataset.get_context(context),
- toolbar: !!this.options.$sidebar
- }, callback);
- }
+ return this.load_view(context);
},
/**
* re-renders the content of the list view
*/
reload_content: function () {
var self = this;
- self.$element.find('.oe_list_record_selector').prop('checked', false);
+ self.$el.find('.oe_list_record_selector').prop('checked', false);
this.records.reset();
var reloaded = $.Deferred();
- this.$element.find('.oe_list_content').append(
+ this.$el.find('.oe_list_content').append(
this.groups.render(function () {
if (self.dataset.index == null) {
var has_one = false;
return this.reload_content();
},
reload_record: function (record) {
+ var self = this;
return this.dataset.read_ids(
[record.get('id')],
_.pluck(_(this.columns).filter(function (r) {
return r.tag === 'field';
}), 'name')
- ).then(function (records) {
+ ).done(function (records) {
+ if (!records[0]) {
+ self.records.remove(record);
+ return;
+ }
_(records[0]).each(function (value, key) {
record.set(key, value, {silent: true});
});
*/
do_search: function (domain, context, group_by) {
this.page = 0;
- this.groups.datagroup = new instance.web.DataGroup(
+ this.groups.datagroup = new DataGroup(
this, this.model, domain, context, group_by);
this.groups.datagroup.sort = this.dataset._sort;
group_by = null;
}
this.no_leaf = !!context['group_by_no_leaf'];
+ this.grouped = !!group_by;
- this.reload_view(!!group_by, context).then(
+ return this.load_view(context).then(
this.proxy('reload_content'));
},
/**
return;
}
var self = this;
- return $.when(this.dataset.unlink(ids)).then(function () {
+ return $.when(this.dataset.unlink(ids)).done(function () {
_(ids).each(function (id) {
self.records.remove(self.records.get(id));
});
if (!ids.length) {
this.dataset.index = 0;
if (this.sidebar) {
- this.sidebar.$element.hide();
+ this.sidebar.$el.hide();
}
this.compute_aggregates();
return;
this.dataset.index = _(this.dataset.ids).indexOf(ids[0]);
if (this.sidebar) {
- this.sidebar.$element.show();
+ this.options.$sidebar.show();
+ this.sidebar.$el.show();
}
this.compute_aggregates(_(records).map(function (record) {
* Base handling of buttons, can be called when overriding do_button_action
* in order to bypass parent overrides.
*
+ * The callback will be provided with the ``id`` as its parameter, in case
+ * handle_button's caller had to alter the ``id`` (or even create one)
+ * while not being ``callback``'s creator.
+ *
* This method should not be overridden.
*
* @param {String} name action name
c.add(action.context);
}
action.context = c;
- this.do_execute_action(action, this.dataset, id, callback);
+ this.do_execute_action(
+ action, this.dataset, id, _.bind(callback, null, id));
},
/**
* Handles the activation of a record (clicking on it)
},
display_aggregates: function (aggregation) {
var self = this;
- var $footer_cells = this.$element.find('.oe_list_footer');
+ var $footer_cells = this.$el.find('.oe_list_footer');
_(this.aggregate_columns).each(function (column) {
if (!column['function']) {
return;
}
$footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id))
- .html(instance.web.format_cell(aggregation, column, {
- process_modifiers: false
- }));
+ .html(column.format(aggregation, { process_modifiers: false }));
});
},
get_selected_ids: function() {
pad_columns: function (count, options) {
options = options || {};
// padding for action/pager header
- var $first_header = this.$element.find('thead tr:first th');
+ var $first_header = this.$el.find('thead tr:first th');
var colspan = $first_header.attr('colspan');
if (colspan) {
if (!this.previous_colspan) {
$first_header.attr('colspan', parseInt(colspan, 10) + count);
}
// Padding for column titles, footer and data rows
- var $rows = this.$element
+ var $rows = this.$el
.find('.oe_list_header_columns, tr:not(thead tr)')
.not(options['except']);
var newcols = new Array(count+1).join('<td class="oe_list_padding"></td>');
* Removes all padding columns of the table
*/
unpad_columns: function () {
- this.$element.find('.oe_list_padding').remove();
+ this.$el.find('.oe_list_padding').remove();
if (this.previous_colspan) {
- this.$element
+ this.$el
.find('thead tr:first th')
.attr('colspan', this.previous_colspan);
this.previous_colspan = null;
}
},
no_result: function () {
- this.$element.find('.oe_view_nocontent').remove();
+ this.$el.find('.oe_view_nocontent').remove();
if (this.groups.group_by
|| !this.options.action
|| !this.options.action.help) {
return;
}
- this.$element.find('table:first').hide();
- this.$element.prepend(
+ this.$el.find('table:first').hide();
+ this.$el.prepend(
$('<div class="oe_view_nocontent">').html(this.options.action.help)
);
+ var create_nocontent = this.$buttons;
+ this.$el.find('.oe_view_nocontent').click(function() {
+ create_nocontent.openerpBounce();
+ });
}
});
instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.ListView.List# */{
var $row;
if (attribute === 'id') {
if (old_value) {
- throw new Error("Setting 'id' attribute on existing record "
- + JSON.stringify(record.attributes));
+ throw new Error(_.str.sprintf( _t("Setting 'id' attribute on existing record %s"),
+ JSON.stringify(record.attributes) ));
}
if (!_.contains(self.dataset.ids, value)) {
// add record to dataset if not already in (added by
this.records.bind(event, callback);
}, this);
- this.$_element = $('<tbody>')
- .appendTo(document.body)
+ this.$current = $('<tbody>')
+ .delegate('input[readonly=readonly]', 'click', function (e) {
+ /*
+ Against all logic and sense, as of right now @readonly
+ apparently does nothing on checkbox and radio inputs, so
+ the trick of using @readonly to have, well, readonly
+ checkboxes (which still let clicks go through) does not
+ work out of the box. We *still* need to preventDefault()
+ on the event, otherwise the checkbox's state *will* toggle
+ on click
+ */
+ e.preventDefault();
+ })
.delegate('th.oe_list_record_selector', 'click', function (e) {
e.stopPropagation();
var selection = self.get_selection();
field = $target.closest('td').data('field'),
$row = $target.closest('tr'),
record_id = self.row_id($row);
+
+ if ($target.attr('disabled')) {
+ return;
+ }
+ $target.attr('disabled', 'disabled');
// note: $.data converts data to number if it's composed only
// of digits, nice when storing actual numbers, not nice when
// storing strings composed only of digits. Force the action
// name to be a string
- $(self).trigger('action', [field.toString(), record_id, function () {
- return self.reload_record(self.records.get(record_id));
+ $(self).trigger('action', [field.toString(), record_id, function (id) {
+ $target.removeAttr('disabled');
+ return self.reload_record(self.records.get(id));
}]);
})
.delegate('a', 'click', function (e) {
if (row_id) {
e.stopPropagation();
if (!self.dataset.select_id(row_id)) {
- throw new Error("Could not find id in dataset");
+ throw new Error(_t("Could not find id in dataset"));
}
self.row_clicked(e);
}
// set) a human-readable version. m2o does not have this issue
// because the non-human-readable is just a number, where the
// human-readable version is a pair
- if (value && (ref_match = /([\w\.]+),(\d+)/.exec(value))) {
+ if (value && (ref_match = /^([\w\.]+),(\d+)$/.exec(value))) {
// reference values are in the shape "$model,$id" (as a
// string), we need to split and name_get this pair in order
// to get a correctly displayable value in the field
var model = ref_match[1],
id = parseInt(ref_match[2], 10);
- new instance.web.DataSet(this.view, model).name_get([id], function(names) {
+ new instance.web.DataSet(this.view, model).name_get([id]).done(function(names) {
if (!names.length) { return; }
record.set(column.id, names[0][1]);
});
// and let the various registered events handle refreshing the
// row
new instance.web.DataSet(this.view, column.relation)
- .name_get([value], function (names) {
+ .name_get([value]).done(function (names) {
if (!names.length) { return; }
record.set(column.id, names[0]);
});
}
+ } else if (column.type === 'many2many') {
+ value = record.get(column.id);
+ // non-resolved (string) m2m values are arrays
+ if (value instanceof Array && !_.isEmpty(value)
+ && !record.get(column.id + '__display')) {
+ var ids;
+ // they come in two shapes:
+ if (value[0] instanceof Array) {
+ var command = value[0];
+ // 1. an array of m2m commands (usually (6, false, ids))
+ if (command[0] !== 6) {
+ throw new Error(_.str.sprintf( _t("Unknown m2m command %s"), command[0]));
+ }
+ ids = command[2];
+ } else {
+ // 2. an array of ids
+ ids = value;
+ }
+ new instance.web.Model(column.relation)
+ .call('name_get', [ids]).done(function (names) {
+ // FIXME: nth horrible hack in this poor listview
+ record.set(column.id + '__display',
+ _(names).pluck(1).join(', '));
+ record.set(column.id, ids);
+ });
+ // temp empty value
+ record.set(column.id, false);
+ }
}
- return instance.web.format_cell(record.toForm().data, column, {
+ return column.format(record.toForm().data, {
model: this.dataset.model,
id: record.get('id')
});
},
render: function () {
- var self = this;
- if (this.$current) {
- this.$current.remove();
- }
- this.$current = this.$_element.clone(true);
this.$current.empty().append(
QWeb.render('ListView.rows', _.extend({
render_cell: function () {
}, this);
if (!this.$current) { return; }
this.$current.remove();
- this.$current = null;
- this.$_element.remove();
},
get_records: function () {
return this.records.map(function (record) {
passtrough_events: 'action deleted row_link',
/**
* Grouped display for the ListView. Handles basic DOM events and interacts
- * with the :js:class:`~instance.web.DataGroup` bound to it.
+ * with the :js:class:`~DataGroup` bound to it.
*
* Provides events similar to those of
* :js:class:`~instance.web.ListView.List`
.replaceWith(self.render());
});
this.$row.children().last()
+ .addClass('oe_list_group_pagination')
.append($prev)
.append('<span class="oe_list_pager_state"></span>')
.append($next);
self.bind_child_events(child);
child.datagroup = group;
- var $row = child.$row = $('<tr>');
+ var $row = child.$row = $('<tr class="oe_group_header">');
if (group.openable && group.length) {
$row.click(function (e) {
if (!$row.data('open')) {
_t("Grouping on field '%s' is not possible because that field does not appear in the list view."),
group.grouped_on));
}
+ var group_label;
try {
- $group_column.html(instance.web.format_cell(
- row_data, group_column, {
- value_if_empty: _t("Undefined"),
- process_modifiers: false
- }));
+ group_label = group_column.format(row_data, {
+ value_if_empty: _t("Undefined"),
+ process_modifiers: false
+ });
} catch (e) {
- $group_column.html(row_data[group_column.id].value);
+ group_label = _.str.escapeHTML(row_data[group_column.id].value);
}
+ // group_label is html-clean (through format or explicit
+ // escaping if format failed), can inject straight into HTML
+ $group_column.html(_.str.sprintf(_t("%s (%d)"),
+ group_label, group.length));
+
if (group.length && group.openable) {
// Make openable if not terminal group & group_by_no_leaf
$group_column.prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">');
}
}
self.indent($group_column, group.level);
- // count column
- $('<td>').text(group.length).appendTo($row);
if (self.options.selectable) {
$row.append('<td>');
}
_(self.columns).chain()
- .filter(function (column) {return !column.invisible;})
+ .filter(function (column) { return column.invisible !== '1'; })
.each(function (column) {
if (column.meta) {
// do not do anything
var r = {};
r[column.id] = {value: group.aggregates[column.id]};
$('<td class="oe_number">')
- .html(instance.web.format_cell(
- r, column, {process_modifiers: false}))
+ .html(column.format(r, {process_modifiers: false}))
.appendTo($row);
} else {
$row.append('<td>');
var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name');
var options = { offset: page * limit, limit: limit, context: {bin_size: true} };
//TODO xmo: investigate why we need to put the setTimeout
- $.async_when().then(function() {dataset.read_slice(fields, options).then(function (records) {
- // FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently
- if (self.records.length) {
- self.records.reset(null, {silent: true});
- }
- if (!self.datagroup.openable) {
- view.configure_pager(dataset);
- } else {
- if (dataset.size() == records.length) {
- // only one page
- self.$row.find('td.oe_list_group_pagination').empty();
+ $.async_when().done(function() {
+ dataset.read_slice(fields, options).done(function (records) {
+ // FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently
+ if (self.records.length) {
+ self.records.reset(null, {silent: true});
+ }
+ if (!self.datagroup.openable) {
+ view.configure_pager(dataset);
} else {
- var pages = Math.ceil(dataset.size() / limit);
- self.$row
- .find('.oe_list_pager_state')
- .text(_.str.sprintf(_t("%(page)d/%(page_count)d"), {
- page: page + 1,
- page_count: pages
- }))
- .end()
- .find('button[data-pager-action=previous]')
- .attr('disabled', page === 0)
- .end()
- .find('button[data-pager-action=next]')
- .attr('disabled', page === pages - 1);
+ if (dataset.size() == records.length) {
+ // only one page
+ self.$row.find('td.oe_list_group_pagination').empty();
+ } else {
+ var pages = Math.ceil(dataset.size() / limit);
+ self.$row
+ .find('.oe_list_pager_state')
+ .text(_.str.sprintf(_t("%(page)d/%(page_count)d"), {
+ page: page + 1,
+ page_count: pages
+ }))
+ .end()
+ .find('button[data-pager-action=previous]')
+ .css('visibility',
+ page === 0 ? 'hidden' : '')
+ .end()
+ .find('button[data-pager-action=next]')
+ .css('visibility',
+ page === pages - 1 ? 'hidden' : '');
+ }
}
- }
- self.records.add(records, {silent: true});
- list.render();
- d.resolve(list);
- if (_.isEmpty(records)) {
- view.no_result();
- }
- });});
+ self.records.add(records, {silent: true});
+ list.render();
+ d.resolve(list);
+ if (_.isEmpty(records)) {
+ view.no_result();
+ }
+ });
+ });
return d.promise();
},
setup_resequence_rows: function (list, dataset) {
// drag and drop enabled if list is not sorted and there is a
- // "sequence" column in the view.
+ // visible column with @widget=handle or "sequence" column in the view.
if ((dataset.sort && dataset.sort())
|| !_(this.columns).any(function (column) {
- return column.name === 'sequence'; })) {
+ return column.widget === 'handle'
+ || column.name === 'sequence'; })) {
return;
}
+ var sequence_field = _(this.columns).find(function (c) {
+ return c.widget === 'handle';
+ });
+ var seqname = sequence_field ? sequence_field.name : 'sequence';
+
// ondrop, move relevant record & fix sequences
list.$current.sortable({
axis: 'y',
items: '> tr[data-id]',
- containment: 'parent',
- helper: 'clone',
+ helper: 'clone'
+ });
+ if (sequence_field) {
+ list.$current.sortable('option', 'handle', '.oe_list_field_handle');
+ }
+ list.$current.sortable('option', {
+ start: function (e, ui) {
+ ui.placeholder.height(ui.item.height());
+ },
stop: function (event, ui) {
var to_move = list.records.get(ui.item.data('id')),
target_id = ui.item.prev().data('id'),
var record, index = to,
// if drag to 1st row (to = 0), start sequencing from 0
// (exclusive lower bound)
- seq = to ? list.records.at(to - 1).get('sequence') : 0;
+ seq = to ? list.records.at(to - 1).get(seqname) : 0;
while (++seq, record = list.records.at(index++)) {
// write are independent from one another, so we can just
// launch them all at the same time and we don't really
// Accounting > Taxes > Taxes, child tax accounts)
// when synchronous (without setTimeout)
(function (dataset, id, seq) {
- $.async_when().then(function () {
- dataset.write(id, {sequence: seq});
+ $.async_when().done(function () {
+ var attrs = {};
+ attrs[seqname] = seq;
+ dataset.write(id, attrs);
});
}(dataset, record.get('id'), seq));
- record.set('sequence', seq);
+ record.set(seqname, seq);
}
}
});
},
render: function (post_render) {
var self = this;
- var $element = $('<tbody>');
- this.elements = [$element[0]];
+ var $el = $('<tbody>');
+ this.elements = [$el[0]];
this.datagroup.list(
_(this.view.visible_columns).chain()
.filter(function (column) { return column.tag === 'field' })
.pluck('name').value(),
function (groups) {
- $element[0].appendChild(
+ $el[0].appendChild(
self.render_groups(groups));
if (post_render) { post_render(); }
}, function (dataset) {
- self.render_dataset(dataset).then(function (list) {
+ self.render_dataset(dataset).done(function (list) {
self.children[null] = list;
self.elements =
- [list.$current.replaceAll($element)[0]];
+ [list.$current.replaceAll($el)[0]];
self.setup_resequence_rows(list, dataset);
if (post_render) { post_render(); }
});
});
- return $element;
+ return $el;
},
/**
* Returns the ids of all selected records for this group, and the records
}
});
+var DataGroup = instance.web.Class.extend({
+ init: function(parent, model, domain, context, group_by, level) {
+ this.model = new instance.web.Model(model, context, domain);
+ this.group_by = group_by;
+ this.context = context;
+ this.domain = domain;
+
+ this.level = level || 0;
+ },
+ list: function (fields, ifGroups, ifRecords) {
+ var self = this;
+ var query = this.model.query(fields).order_by(this.sort).group_by(this.group_by);
+ $.when(query).done(function (querygroups) {
+ // leaf node
+ if (!querygroups) {
+ var ds = new instance.web.DataSetSearch(self, self.model.name, self.model.context(), self.model.domain());
+ ds._sort = self.sort;
+ ifRecords(ds);
+ return;
+ }
+ // internal node
+ var child_datagroups = _(querygroups).map(function (group) {
+ var child_context = _.extend(
+ {}, self.model.context(), group.model.context());
+ var child_dg = new DataGroup(
+ self, self.model.name, group.model.domain(),
+ child_context, group.model._context.group_by,
+ self.level + 1);
+ child_dg.sort = self.sort;
+ // copy querygroup properties
+ child_dg.__context = child_context;
+ child_dg.__domain = group.model.domain();
+ child_dg.folded = group.get('folded');
+ child_dg.grouped_on = group.get('grouped_on');
+ child_dg.length = group.get('length');
+ child_dg.value = group.get('value');
+ child_dg.openable = group.get('has_children');
+ child_dg.aggregates = group.get('aggregates');
+ return child_dg;
+ });
+ ifGroups(child_datagroups);
+ });
+ }
+});
+var StaticDataGroup = DataGroup.extend({
+ init: function (dataset) {
+ this.dataset = dataset;
+ },
+ list: function (fields, ifGroups, ifRecords) {
+ ifRecords(this.dataset);
+ }
+});
+
/**
* @mixin Events
*/
} else if (val instanceof Array) {
output[k] = val[0];
} else {
- throw new Error("Can't convert value " + val + " to context");
+ throw new Error(_.str.sprintf(_t("Can't convert value %s to context"), val));
}
}
return output;
Events: Events,
Record: Record,
Collection: Collection
-}
+};
+/**
+ * Registry for column objects used to format table cells (and some other tasks
+ * e.g. aggregation computations).
+ *
+ * Maps a field or button to a Column type via its ``$tag.$widget``,
+ * ``$tag.$type`` or its ``$tag`` (alone).
+ *
+ * This specific registry has a dedicated utility method ``for_`` taking a
+ * field (from fields_get/fields_view_get.field) and a node (from a view) and
+ * returning the right object *already instantiated from the data provided*.
+ *
+ * @type {instance.web.Registry}
+ */
+instance.web.list.columns = new instance.web.Registry({
+ 'field': 'instance.web.list.Column',
+ 'field.boolean': 'instance.web.list.Boolean',
+ 'field.binary': 'instance.web.list.Binary',
+ 'field.progressbar': 'instance.web.list.ProgressBar',
+ 'field.handle': 'instance.web.list.Handle',
+ 'button': 'instance.web.list.Button',
+ 'field.many2onebutton': 'instance.web.list.Many2OneButton',
+ 'field.many2many': 'instance.web.list.Many2Many'
+});
+instance.web.list.columns.for_ = function (id, field, node) {
+ var description = _.extend({tag: node.tag}, field, node.attrs);
+ var tag = description.tag;
+ var Type = this.get_any([
+ tag + '.' + description.widget,
+ tag + '.'+ description.type,
+ tag
+ ]);
+ return new Type(id, node.tag, description)
+};
+
+instance.web.list.Column = instance.web.Class.extend({
+ init: function (id, tag, attrs) {
+ _.extend(attrs, {
+ id: id,
+ tag: tag
+ });
+
+ this.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
+ delete attrs.modifiers;
+ _.extend(this, attrs);
+
+ if (this.modifiers['tree_invisible']) {
+ this.invisible = '1';
+ } else { delete this.invisible; }
+ },
+ modifiers_for: function (fields) {
+ var out = {};
+ var domain_computer = instance.web.form.compute_domain;
+
+ for (var attr in this.modifiers) {
+ if (!this.modifiers.hasOwnProperty(attr)) { continue; }
+ var modifier = this.modifiers[attr];
+ out[attr] = _.isBoolean(modifier)
+ ? modifier
+ : domain_computer(modifier, fields);
+ }
+
+ return out;
+ },
+ to_aggregate: function () {
+ if (this.type !== 'integer' && this.type !== 'float') {
+ return {};
+ }
+ var aggregation_func = this['group_operator'] || 'sum';
+ if (!(aggregation_func in this)) {
+ return {};
+ }
+ var C = function (fn, label) {
+ this['function'] = fn;
+ this.label = label;
+ };
+ C.prototype = this;
+ return new C(aggregation_func, this[aggregation_func]);
+ },
+ /**
+ *
+ * @param row_data record whose values should be displayed in the cell
+ * @param {Object} [options]
+ * @param {String} [options.value_if_empty=''] what to display if the field's value is ``false``
+ * @param {Boolean} [options.process_modifiers=true] should the modifiers be computed ?
+ * @param {String} [options.model] current record's model
+ * @param {Number} [options.id] current record's id
+ * @return {String}
+ */
+ format: function (row_data, options) {
+ options = options || {};
+ var attrs = {};
+ if (options.process_modifiers !== false) {
+ attrs = this.modifiers_for(row_data);
+ }
+ if (attrs.invisible) { return ''; }
+
+ if (!row_data[this.id]) {
+ return options.value_if_empty === undefined
+ ? ''
+ : options.value_if_empty;
+ }
+ return this._format(row_data, options);
+ },
+ /**
+ * Method to override in order to provide alternative HTML content for the
+ * cell. Column._format will simply call ``instance.web.format_value`` and
+ * escape the output.
+ *
+ * The output of ``_format`` will *not* be escaped by ``format``, any
+ * escaping *must be done* by ``format``.
+ *
+ * @private
+ */
+ _format: function (row_data, options) {
+ return _.escape(instance.web.format_value(
+ row_data[this.id].value, this, options.value_if_empty));
+ }
+});
+instance.web.list.MetaColumn = instance.web.list.Column.extend({
+ meta: true,
+ init: function (id, string) {
+ this._super(id, '', {string: string});
+ }
+});
+instance.web.list.Button = instance.web.list.Column.extend({
+ /**
+ * Return an actual ``<button>`` tag
+ */
+ format: function (row_data, options) {
+ options = options || {};
+ var attrs = {};
+ if (options.process_modifiers !== false) {
+ attrs = this.modifiers_for(row_data);
+ }
+ if (attrs.invisible) { return ''; }
+
+ return QWeb.render('ListView.row.button', {
+ widget: this,
+ prefix: instance.session.prefix,
+ disabled: attrs.readonly
+ || isNaN(row_data.id.value)
+ || instance.web.BufferedDataSet.virtual_id_regex.test(row_data.id.value)
+ });
+ }
+});
+instance.web.list.Boolean = instance.web.list.Column.extend({
+ /**
+ * Return a potentially disabled checkbox input
+ *
+ * @private
+ */
+ _format: function (row_data, options) {
+ return _.str.sprintf('<input type="checkbox" %s readonly="readonly"/>',
+ row_data[this.id].value ? 'checked="checked"' : '');
+ }
+});
+instance.web.list.Binary = instance.web.list.Column.extend({
+ /**
+ * Return a link to the binary data as a file
+ *
+ * @private
+ */
+ _format: function (row_data, options) {
+ var text = _t("Download");
+ var value = row_data[this.id].value;
+ var download_url;
+ if (value && value.substr(0, 10).indexOf(' ') == -1) {
+ download_url = "data:application/octet-stream;base64," + value;
+ } else {
+ download_url = this.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
+ if (this.filename) {
+ download_url += '&filename_field=' + this.filename;
+ }
+ }
+ if (this.filename && row_data[this.filename]) {
+ text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value(
+ row_data[this.filename].value, {type: 'char'}));
+ }
+ return _.template('<a href="<%-href%>"><%-text%></a> (<%-size%>)', {
+ text: text,
+ href: download_url,
+ size: instance.web.binary_to_binsize(value),
+ });
+ }
+});
+instance.web.list.ProgressBar = instance.web.list.Column.extend({
+ /**
+ * Return a formatted progress bar display
+ *
+ * @private
+ */
+ _format: function (row_data, options) {
+ return _.template(
+ '<progress value="<%-value%>" max="100"><%-value%>%</progress>', {
+ value: _.str.sprintf("%.0f", row_data[this.id].value || 0)
+ });
+ }
+});
+instance.web.list.Handle = instance.web.list.Column.extend({
+ init: function () {
+ this._super.apply(this, arguments);
+ // Handle overrides the field to not be form-editable.
+ this.modifiers.readonly = true;
+ },
+ /**
+ * Return styling hooks for a drag handle
+ *
+ * @private
+ */
+ _format: function (row_data, options) {
+ return '<div class="oe_list_handle">';
+ }
+});
+instance.web.list.Many2OneButton = instance.web.list.Column.extend({
+ _format: function (row_data, options) {
+ this.has_value = !!row_data[this.id].value;
+ this.icon = this.has_value ? 'gtk-yes' : 'gtk-no';
+ this.string = this.has_value ? _t('View') : _t('Create');
+ return QWeb.render('Many2OneButton.cell', {
+ 'widget': this,
+ 'prefix': instance.session.prefix,
+ });
+ },
+});
+instance.web.list.Many2Many = instance.web.list.Column.extend({
+ _format: function (row_data, options) {
+ if (!_.isEmpty(row_data[this.id].value)) {
+ // If value, use __display version for printing
+ row_data[this.id] = row_data[this.id + '__display'];
+ }
+ return this._super(row_data, options);
+ }
+});
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: