[FIX] split formatting of list cells to formatting objects
authorXavier Morel <xmo@openerp.com>
Wed, 8 Aug 2012 10:34:04 +0000 (12:34 +0200)
committerXavier Morel <xmo@openerp.com>
Wed, 8 Aug 2012 10:34:04 +0000 (12:34 +0200)
simpler to override in order to have new list field widgets

bzr revid: xmo@openerp.com-20120808103404-jj6w04x2mp2lrwl1

addons/web/static/src/js/formats.js
addons/web/static/src/js/view_list.js
doc/list-view.rst

index 6aae967..797e1a2 100644 (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));
-}
-
 };
index d78a732..986b0e4 100644 (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:
index 24c7bb3..91edf42 100644 (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
 ++++++++++++++++++