[FIX] split formatting of list cells to formatting objects
simpler to override in order to have new list field widgets bzr revid: xmo@openerp.com-20120808103404-jj6w04x2mp2lrwl1
This commit is contained in:
parent
2447e00d22
commit
5c225be25e
|
@ -271,84 +271,4 @@ instance.web.auto_date_to_str = function(value, type) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a provided cell based on its field type. Most of the field types
|
|
||||||
* return a correctly formatted value, but some tags and fields are
|
|
||||||
* special-cased in their handling:
|
|
||||||
*
|
|
||||||
* * buttons will return an actual ``<button>`` tag with a bunch of error handling
|
|
||||||
*
|
|
||||||
* * boolean fields will return a checkbox input, potentially disabled
|
|
||||||
*
|
|
||||||
* * binary fields will return a link to download the binary data as a file
|
|
||||||
*
|
|
||||||
* @param {Object} row_data record whose values should be displayed in the cell
|
|
||||||
* @param {Object} column column descriptor
|
|
||||||
* @param {"button"|"field"} column.tag base control type
|
|
||||||
* @param {String} column.type widget type for a field control
|
|
||||||
* @param {String} [column.string] button label
|
|
||||||
* @param {String} [column.icon] button icon
|
|
||||||
* @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
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
instance.web.format_cell = function (row_data, column, options) {
|
|
||||||
options = options || {};
|
|
||||||
var attrs = {};
|
|
||||||
if (options.process_modifiers !== false) {
|
|
||||||
attrs = column.modifiers_for(row_data);
|
|
||||||
}
|
|
||||||
if (attrs.invisible) { return ''; }
|
|
||||||
|
|
||||||
if (column.tag === 'button') {
|
|
||||||
return _.template('<button type="button" title="<%-title%>" <%=additional_attributes%> >' +
|
|
||||||
'<img src="<%-prefix%>/web/static/src/img/icons/<%-icon%>.png" alt="<%-alt%>"/>' +
|
|
||||||
'</button>', {
|
|
||||||
title: column.string || '',
|
|
||||||
additional_attributes: isNaN(row_data["id"].value) && instance.web.BufferedDataSet.virtual_id_regex.test(row_data["id"].value) ?
|
|
||||||
'disabled="disabled" class="oe_list_button_disabled"' : '',
|
|
||||||
prefix: instance.connection.prefix,
|
|
||||||
icon: column.icon,
|
|
||||||
alt: column.string || ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!row_data[column.id]) {
|
|
||||||
return options.value_if_empty === undefined ? '' : options.value_if_empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (column.widget || column.type) {
|
|
||||||
case "boolean":
|
|
||||||
return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>',
|
|
||||||
row_data[column.id].value ? 'checked="checked"' : '');
|
|
||||||
case "binary":
|
|
||||||
var text = _t("Download"),
|
|
||||||
download_url = _.str.sprintf('/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', instance.connection.session_id, options.model, column.id, options.id);
|
|
||||||
if (column.filename) {
|
|
||||||
download_url += '&filename_field=' + column.filename;
|
|
||||||
if (row_data[column.filename]) {
|
|
||||||
text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value(
|
|
||||||
row_data[column.filename].value, {type: 'char'}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', {
|
|
||||||
text: text,
|
|
||||||
href: download_url,
|
|
||||||
size: row_data[column.id].value
|
|
||||||
});
|
|
||||||
case 'progressbar':
|
|
||||||
return _.template(
|
|
||||||
'<progress value="<%-value%>" max="100"><%-value%>%</progress>', {
|
|
||||||
value: _.str.sprintf("%.0f", row_data[column.id].value || 0)
|
|
||||||
});
|
|
||||||
case 'handle':
|
|
||||||
return '<div class="oe_list_handle">';
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.escape(instance.web.format_value(
|
|
||||||
row_data[column.id].value, column, options.value_if_empty));
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -399,73 +399,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||||
* @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
|
* @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed
|
||||||
*/
|
*/
|
||||||
setup_columns: function (fields, grouped) {
|
setup_columns: function (fields, grouped) {
|
||||||
var domain_computer = instance.web.form.compute_domain;
|
var registry = instance.web.list.columns;
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.columns.splice(0, this.columns.length);
|
this.columns.splice(0, this.columns.length);
|
||||||
this.columns.push.apply(
|
this.columns.push.apply(this.columns,
|
||||||
this.columns,
|
_(this.fields_view.arch.children).map(function (field) {
|
||||||
_(this.fields_view.arch.children).map(field_to_column));
|
var id = field.attrs.name;
|
||||||
|
return registry.for_(id, fields[id], field);
|
||||||
|
}));
|
||||||
if (grouped) {
|
if (grouped) {
|
||||||
this.columns.unshift({
|
this.columns.unshift(
|
||||||
id: '_group', tag: '', string: _t("Group"), meta: true,
|
new instance.web.list.MetaColumn('_group', _t("Group")));
|
||||||
modifiers_for: function () { return {}; },
|
|
||||||
modifiers: {}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.visible_columns = _.filter(this.columns, function (column) {
|
this.visible_columns = _.filter(this.columns, function (column) {
|
||||||
return column.invisible !== '1';
|
return column.invisible !== '1';
|
||||||
});
|
});
|
||||||
|
|
||||||
this.aggregate_columns = _(this.visible_columns)
|
this.aggregate_columns = _(this.visible_columns).invoke('to_aggregate');
|
||||||
.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]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Used to handle a click on a table row, if no other handler caught the
|
* Used to handle a click on a table row, if no other handler caught the
|
||||||
|
@ -820,9 +770,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||||
}
|
}
|
||||||
|
|
||||||
$footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id))
|
$footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id))
|
||||||
.html(instance.web.format_cell(aggregation, column, {
|
.html(column.format(aggregation, { process_modifiers: false }));
|
||||||
process_modifiers: false
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
get_selected_ids: function() {
|
get_selected_ids: function() {
|
||||||
|
@ -1058,7 +1006,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return instance.web.format_cell(record.toForm().data, column, {
|
return column.format(record.toForm().data, {
|
||||||
model: this.dataset.model,
|
model: this.dataset.model,
|
||||||
id: record.get('id')
|
id: record.get('id')
|
||||||
});
|
});
|
||||||
|
@ -1345,10 +1293,9 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
|
||||||
}
|
}
|
||||||
var group_label;
|
var group_label;
|
||||||
try {
|
try {
|
||||||
group_label = instance.web.format_cell(
|
group_label = group_column.format(row_data, {
|
||||||
row_data, group_column, {
|
value_if_empty: _t("Undefined"),
|
||||||
value_if_empty: _t("Undefined"),
|
process_modifiers: false
|
||||||
process_modifiers: false
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
group_label = row_data[group_column.id].value;
|
group_label = row_data[group_column.id].value;
|
||||||
|
@ -1372,7 +1319,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
|
||||||
$row.append('<td>');
|
$row.append('<td>');
|
||||||
}
|
}
|
||||||
_(self.columns).chain()
|
_(self.columns).chain()
|
||||||
.filter(function (column) {return !column.invisible;})
|
.filter(function (column) { return column.invisible !== '1'; })
|
||||||
.each(function (column) {
|
.each(function (column) {
|
||||||
if (column.meta) {
|
if (column.meta) {
|
||||||
// do not do anything
|
// do not do anything
|
||||||
|
@ -1380,8 +1327,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
|
||||||
var r = {};
|
var r = {};
|
||||||
r[column.id] = {value: group.aggregates[column.id]};
|
r[column.id] = {value: group.aggregates[column.id]};
|
||||||
$('<td class="oe_number">')
|
$('<td class="oe_number">')
|
||||||
.html(instance.web.format_cell(
|
.html(column.format(r, {process_modifiers: false}))
|
||||||
r, column, {process_modifiers: false}))
|
|
||||||
.appendTo($row);
|
.appendTo($row);
|
||||||
} else {
|
} else {
|
||||||
$row.append('<td>');
|
$row.append('<td>');
|
||||||
|
@ -1979,6 +1925,204 @@ instance.web.list = {
|
||||||
Events: Events,
|
Events: Events,
|
||||||
Record: Record,
|
Record: Record,
|
||||||
Collection: Collection
|
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',
|
||||||
|
});
|
||||||
|
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 (label, fn) {
|
||||||
|
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) {
|
||||||
|
return _.template('<button type="button" title="<%-title%>" <%=additional_attributes%> >' +
|
||||||
|
'<img src="<%-prefix%>/web/static/src/img/icons/<%-icon%>.png" alt="<%-alt%>"/>' +
|
||||||
|
'</button>', {
|
||||||
|
title: this.string || '',
|
||||||
|
additional_attributes: isNaN(row_data["id"].value) && instance.web.BufferedDataSet.virtual_id_regex.test(row_data["id"].value) ?
|
||||||
|
'disabled="disabled" class="oe_list_button_disabled"' : '',
|
||||||
|
prefix: instance.connection.prefix,
|
||||||
|
icon: this.icon,
|
||||||
|
alt: this.string || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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 disabled="disabled"/>',
|
||||||
|
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 download_url = _.str.sprintf(
|
||||||
|
'/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d',
|
||||||
|
instance.connection.session_id, options.model, this.id, options.id);
|
||||||
|
if (this.filename) {
|
||||||
|
download_url += '&filename_field=' + this.filename;
|
||||||
|
if (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: row_data[this.id].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({
|
||||||
|
/**
|
||||||
|
* Return styling hooks for a drag handle
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_format: function (row_data, options) {
|
||||||
|
return '<div class="oe_list_handle">';
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
|
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
|
||||||
|
|
|
@ -60,6 +60,56 @@ various situations:
|
||||||
|
|
||||||
Selector cells
|
Selector cells
|
||||||
|
|
||||||
|
Columns display customization
|
||||||
|
+++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
The list view provides a registry to
|
||||||
|
:js:class:`openerp.web.list.Column` objects allowing for the
|
||||||
|
customization of a column's display (e.g. so that a binary field is
|
||||||
|
rendered as a link to the binary file directly in the list view).
|
||||||
|
|
||||||
|
The registry is ``instance.web.list.columns``, the keys are of the
|
||||||
|
form ``tag.type`` where ``tag`` can be ``field`` or ``button``, and
|
||||||
|
``type`` can be either the field's type or the field's ``@widget`` (in
|
||||||
|
the view).
|
||||||
|
|
||||||
|
Most of the time, you'll want to define a ``tag.widget`` key
|
||||||
|
(e.g. ``field.progressbar``).
|
||||||
|
|
||||||
|
.. js:class:: openerp.web.list.Column(id, tag, attrs)
|
||||||
|
|
||||||
|
.. js:method:: openerp.web.list.Column.format(record_data, options)
|
||||||
|
|
||||||
|
Top-level formatting method, returns an empty string if the
|
||||||
|
column is invisible (unless the ``process_modifiers=false``
|
||||||
|
option is provided); returns ``options.value_if_empty`` or an
|
||||||
|
empty string if there is no value in the record for the
|
||||||
|
column.
|
||||||
|
|
||||||
|
Otherwise calls :js:func:`~openerp.web.list.Column._format`
|
||||||
|
and returns its result.
|
||||||
|
|
||||||
|
This method only needs to be overridden if the column has no
|
||||||
|
concept of values (and needs to bypass that check), for a
|
||||||
|
button for instance.
|
||||||
|
|
||||||
|
Otherwise, custom columns should generally override
|
||||||
|
:js:func:`~openerp.web.list.Column._format` instead.
|
||||||
|
|
||||||
|
.. js:method:: openerp,web.list.Column._format(record_data, options)
|
||||||
|
|
||||||
|
Never called directly, called if the column is visible and has
|
||||||
|
a value.
|
||||||
|
|
||||||
|
The default implementation calls
|
||||||
|
:js:func:`~openerp.web.format_value` and htmlescapes the
|
||||||
|
result (via ``_.escape``).
|
||||||
|
|
||||||
|
Note that the implementation of
|
||||||
|
:js:func:`~openerp.web.list.Column._format` *must* escape the
|
||||||
|
data provided to it, its output will *not* be escaped by
|
||||||
|
:js:func:`~openerp.web.list.Column.format`.
|
||||||
|
|
||||||
Editable list view
|
Editable list view
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue