[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 * @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:

View File

@ -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
++++++++++++++++++ ++++++++++++++++++