[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:
Xavier Morel 2012-08-08 12:34:04 +02:00
parent 2447e00d22
commit 5c225be25e
3 changed files with 265 additions and 151 deletions

View File

@ -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));
}
};

View File

@ -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
*/
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: {}
});
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
@ -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))
.html(instance.web.format_cell(aggregation, column, {
process_modifiers: false
}));
.html(column.format(aggregation, { process_modifiers: false }));
});
},
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,
id: record.get('id')
});
@ -1345,10 +1293,9 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
}
var group_label;
try {
group_label = 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_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>');
}
_(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
@ -1380,8 +1327,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
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>');
@ -1979,6 +1925,204 @@ instance.web.list = {
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',
});
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:

View File

@ -60,6 +60,56 @@ various situations:
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
++++++++++++++++++