* Fixed CheckBoxColumn.
authorBradley Ayers <bradley.ayers@enigmainteractive.com>
Fri, 1 Apr 2011 07:50:56 +0000 (17:50 +1000)
committerBradley Ayers <bradley.ayers@enigmainteractive.com>
Fri, 1 Apr 2011 07:50:56 +0000 (17:50 +1000)
* Fixed rendering of column headers. SafeData is now honored.
* Added LinkColumn.

django_tables/columns.py
django_tables/rows.py
django_tables/tables.py
django_tables/templates/django_tables/table.html
django_tables/utils.py

index 5e7f82a6fdf6246144189a1726708e2e62e46e43..05514bea57758a5fc56ff9772d12ac232010ecd4 100644 (file)
@@ -1,8 +1,11 @@
 # -*- coding: utf-8 -*-
+from django.core.urlresolvers import reverse
 from django.utils.encoding import force_unicode, StrAndUnicode
 from django.utils.datastructures import SortedDict
 from django.utils.text import capfirst
-from .utils import OrderBy
+from django.utils.safestring import mark_safe
+from django.template import Context, Template
+from .utils import OrderBy, A, AttributeDict
 
 
 class Column(object):
@@ -16,7 +19,7 @@ class Column(object):
     creation_counter = 0
 
     def __init__(self, verbose_name=None, accessor=None, default=None,
-                 visible=True, sortable=None, formatter=None):
+                 visible=True, sortable=None):
         """Initialise a :class:`Column` object.
 
         :param verbose_name:
@@ -87,19 +90,6 @@ class Column(object):
             If :const:`False`, this column will not be allowed to be used in
             ordering the table.
 
-        :param formatter:
-            A callable object that is used as a final step in formatting the
-            value for a cell. The callable will be passed the string that would
-            have otherwise been displayed in the cell.
-
-            In the following table, cells in the *name* column have upper-case
-            values.
-
-            .. code-block:: python
-
-                class Example(tables.Table):
-                    name = tables.Column(formatter=lambda x: x.upper())
-
         """
         if not (accessor is None or isinstance(accessor, basestring) or
                 callable(accessor)):
@@ -108,9 +98,8 @@ class Column(object):
         if callable(accessor) and default is not None:
             raise TypeError('accessor must be string when default is used, not'
                             ' callable')
-        self.accessor = accessor
+        self.accessor = A(accessor) if accessor else None
         self._default = default
-        self.formatter = formatter
         self.sortable = sortable
         self.verbose_name = verbose_name
         self.visible = visible
@@ -128,43 +117,100 @@ class Column(object):
         """
         return self._default() if callable(self._default) else self._default
 
-    def render(self, table, bound_column, bound_row):
+    def render(self, value, **kwargs):
         """Returns a cell's content.
         This method can be overridden by ``render_FOO`` methods on the table or
         by subclassing :class:`Column`.
 
         """
-        return table.data.data_for_cell(bound_column=bound_column,
-                                        bound_row=bound_row)
+        return value
 
 
 class CheckBoxColumn(Column):
     """A subclass of Column that renders its column data as a checkbox"""
-    def __init__(self, attrs=None, *args, **kwargs):
+    def __init__(self, attrs=None, **extra):
         """
         :param attrs: a dict of HTML element attributes to be added to the
             ``<input>``
 
         """
-        super(CheckBoxColumn, self).__init__(*args, **kwargs)
+        params = {'sortable': False}
+        params.update(extra)
+        super(CheckBoxColumn, self).__init__(**params)
         self.attrs = attrs or {}
-
-    def render(self, bound_column, bound_row):
-        from django.template import Template, Context
-        attrs = {'name': bound_column.name}
+        self.verbose_name = mark_safe('<input type="checkbox"/>')
+
+    def render(self, value, bound_column, **kwargs):
+        attrs = AttributeDict({
+            'type': 'checkbox',
+            'name': bound_column.name,
+            'value': value
+        })
         attrs.update(self.attrs)
-        t = Template('<input type="checkbox" value="{{ value }}" '
-                     '{% for attr, value in attrs.iteritems %}'
-                     '{{ attr|escapejs }}="{{ value|escapejs }}" '
-                     '{% endfor %}/>')
-        return t.render(Context({
-            'value': self.value(bound_column=bound_column,
-                                bound_row=bound_row),
-            'attrs': attrs,
-        }))
-
-
-class BoundColumn(StrAndUnicode):
+        return mark_safe('<input %s/>' % AttributeDict(attrs).as_html())
+
+
+
+class LinkColumn(Column):
+    def __init__(self, viewname, urlconf=None, args=None, kwargs=None,
+                 current_app=None, attrs=None, **extra):
+        """
+        The first arguments are identical to that of
+        :func:`django.core.urlresolvers.reverse` and allow a URL to be
+        described. The last argument ``attrs`` allows custom HTML attributes to
+        be added to the ``<a>`` tag.
+        """
+        super(LinkColumn, self).__init__(**extra)
+        self.viewname = viewname
+        self.urlconf = urlconf
+        self.args = args
+        self.kwargs = kwargs
+        self.current_app = current_app
+        self.attrs = attrs or {}
+
+    def render(self, value, record, bound_column, **kwargs):
+        params = {}  # args for reverse()
+        if self.viewname:
+            params['viewname'] = (self.viewname.resolve(record)
+                                 if isinstance(self.viewname, A)
+                                 else self.viewname)
+        if self.urlconf:
+            params['urlconf'] = (self.urlconf.resolve(record)
+                                 if isinstance(self.urlconf, A)
+                                 else self.urlconf)
+        if self.args:
+            params['args'] = [a.resolve(record) if isinstance(a, A) else a
+                              for a in self.args]
+        if self.kwargs:
+            params['kwargs'] = self.kwargs
+            for key, value in self.kwargs:
+                if isinstance(value, A):
+                    params['kwargs'][key] = value.resolve(record)
+        if self.current_app:
+            params['current_app'] = self.current_app
+            for key, value in self.current_app:
+                if isinstance(value, A):
+                    params['current_app'][key] = value.resolve(record)
+        url = reverse(**params)
+        html = '<a href="{url}" {attrs}>{value}</a>'.format(
+            url=reverse(**params),
+            attrs=AttributeDict(self.attrs).as_html(),
+            value=value
+        )
+        return mark_safe(html)
+
+
+class TemplateColumn(Column):
+    def __init__(self, template_code=None, **extra):
+        super(TemplateColumn, self).__init__(**extra)
+        self.template_code = template_code
+
+    def render(self, record, **kwargs):
+        t = Template(self.template_code)
+        return t.render(Context({'record': record}))
+
+
+class BoundColumn(object):
     """A *runtime* version of :class:`Column`. The difference between
     :class:`BoundColumn` and :class:`Column`, is that :class:`BoundColumn`
     objects are of the relationship between a :class:`Column` and a
@@ -189,8 +235,7 @@ class BoundColumn(StrAndUnicode):
         self._name = name
 
     def __unicode__(self):
-        s = self.column.verbose_name or self.name.replace('_', ' ')
-        return capfirst(force_unicode(s))
+        return self.verbose_name
 
     @property
     def table(self):
@@ -213,21 +258,13 @@ class BoundColumn(StrAndUnicode):
         data source.
 
         """
-        return self.column.accessor or self.name
+        return self.column.accessor or A(self.name)
 
     @property
     def default(self):
         """Returns the default value for this column."""
         return self.column.default
 
-    @property
-    def formatter(self):
-        """Returns a function or ``None`` that represents the formatter for
-        this column.
-
-        """
-        return self.column.formatter
-
     @property
     def sortable(self):
         """Returns a ``bool`` depending on whether this column is sortable."""
@@ -241,7 +278,8 @@ class BoundColumn(StrAndUnicode):
     @property
     def verbose_name(self):
         """Returns the verbose name for this column."""
-        return self.column.verbose_name
+        return (self.column.verbose_name
+                or capfirst(force_unicode(self.name.replace('_', ' '))))
 
     @property
     def visible(self):
index a6f9ed9479f6fb8ef83eecd5795fc2613b28fe01..d7e9e4ada388ca680d33367039730a5374cae934 100644 (file)
@@ -1,12 +1,14 @@
 # -*- coding: utf-8 -*-
+from django.utils.safestring import EscapeUnicode, SafeData
+
 
 class BoundRow(object):
     """Represents a *specific* row in a table.
 
-    :class:`BoundRow` objects expose rendered versions of raw table data. This
-    means that formatting (via :attr:`Column.formatter` or an overridden
-    :meth:`Column.render` method) is applied to the values from the table's
-    data.
+    :class:`BoundRow` objects are a container that make it easy to access the
+    final 'rendered' values for cells in a row. You can simply iterate over a
+    :class:`BoundRow` object and it will take care to return values rendered
+    using the correct method (e.g. :meth:`Column.render_FOO`)
 
     To access the rendered value of each cell in a row, just iterate over it:
 
@@ -94,15 +96,34 @@ class BoundRow(object):
     def __getitem__(self, name):
         """Returns the final rendered value for a cell in the row, given the
         name of a column.
+
         """
         bound_column = self.table.columns[name]
-        # use custom render_FOO methods on the table
-        custom = getattr(self.table, 'render_%s' % name, None)
-        if custom:
-            return custom(bound_column, self)
-        return bound_column.column.render(table=self.table,
-                                          bound_column=bound_column,
-                                          bound_row=self)
+        raw = bound_column.accessor.resolve(self.record)
+        kwargs = {
+            'value': raw if raw is not None else bound_column.default,
+            'record': self.record,
+            'column': bound_column.column,
+            'bound_column': bound_column,
+            'bound_row': self,
+            'table': self._table,
+        }
+        render_FOO = 'render_' + bound_column.name
+        render = getattr(self.table, render_FOO, bound_column.column.render)
+        try:
+            return render(**kwargs)
+        except TypeError as e:
+            # Let's be helpful and provide a decent error message, since
+            # render() underwent backwards incompatible changes.
+            if e.message.startswith('render() got an unexpected keyword'):
+                if hasattr(self.table, render_FOO):
+                    cls = self.table.__class__.__name__
+                    meth = render_FOO
+                else:
+                    cls = kwargs['column'].__class__.__name__
+                    meth = 'render'
+                msg = 'Did you forget to add **kwargs to %s.%s() ?' % (cls, meth)
+                raise TypeError(e.message + '. ' + msg)
 
     def __contains__(self, item):
         """Check by both row object and column name."""
index a65d7f791550669b70cb95ac19d28e0db6372bc0..3e8afdfc8ac5f99b55c4b7317441762f6803bd27 100644 (file)
@@ -66,9 +66,6 @@ class TableData(object):
                                                 else ('', name))
             # find the accessor name
             column = self._table.columns[name]
-            if not isinstance(column.accessor, basestring):
-                raise TypeError('unable to sort on a column that uses a '
-                                'callable accessor')
             translated.append(prefix + column.accessor)
         return OrderByTuple(translated)
 
@@ -105,20 +102,6 @@ class TableData(object):
             if modified_item is not None:
                 data[i] = modified_item
 
-    def data_for_cell(self, bound_column, bound_row, apply_formatter=True):
-        """Calculate the value of a cell given a bound row and bound column.
-
-        :param formatting:
-            Apply column formatter after retrieving the value from the data.
-
-        """
-        value = Accessor(bound_column.accessor).resolve(bound_row.record)
-        # try and use default value if we've only got 'None'
-        if value is None and bound_column.default is not None:
-            value = bound_column.default()
-        if apply_formatter and bound_column.formatter:
-            value = bound_column.formatter(value)
-        return value
 
     def __getitem__(self, index):
         return (self.list if hasattr(self, 'list') else self.queryset)[index]
index 2681a2a3c7837a615e0422af7e3ca64c715b62e9..72128761ed3d06cac30c6fa4c2128841941f7576 100644 (file)
@@ -6,10 +6,10 @@
         {% for column in table.columns %}
         {% if column.sortable %}
             {% with column.order_by as ob %}
-            <th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% if ob %}{% set_url_param sort=ob.opposite %}{% else %}{% set_url_param sort=column.name %}{% endif %}">{{ column }}</a></th>
+            <th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% if ob %}{% set_url_param sort=ob.opposite %}{% else %}{% set_url_param sort=column.name %}{% endif %}">{{ column.verbose_name }}</a></th>
             {% endwith %}
         {% else %}
-            <th>{{ column }}</th>
+            <th>{{ column.verbose_name }}</th>
         {% endif %}
         {% endfor %}
         </tr>
index 2a6bd834c8cee31c0c806733bfdb66b021712c14..f327df25d7c9f938b087acee81aa2fedc3c9e9ab 100644 (file)
@@ -145,50 +145,46 @@ class OrderByTuple(tuple, StrAndUnicode):
         return _cmp
 
 
-class Accessor(object):
+class Accessor(str):
     SEPARATOR = '.'
 
-    def __init__(self, path):
-        self.path = path
-
     def resolve(self, context):
-        if callable(self.path):
-            return self.path(context)
-        else:
-            # Try to resolve relationships spanning attributes. This is
-            # basically a copy/paste from django/template/base.py in
-            # Variable._resolve_lookup()
-            current = context
-            for bit in self.bits:
-                try:  # dictionary lookup
-                    current = current[bit]
-                except (TypeError, AttributeError, KeyError):
-                    try:  # attribute lookup
-                        current = getattr(current, bit)
-                    except (TypeError, AttributeError):
-                        try:  # list-index lookup
-                            current = current[int(bit)]
-                        except (IndexError, # list index out of range
-                                ValueError, # invalid literal for int()
-                                KeyError,   # dict without `int(bit)` key
-                                TypeError,  # unsubscriptable object
-                                ):
-                            raise ValueError('Failed lookup for key [%s] in %r'
-                                             ', when resolving the accessor %s'
-                                              % (bit, current, self.path))
-                if callable(current):
-                    current = current()
-                # important that we break in None case, or a relationship
-                # spanning across a null-key will raise an exception in the
-                # next iteration, instead of defaulting.
-                if current is None:
-                    break
-            return current
+        # Try to resolve relationships spanning attributes. This is
+        # basically a copy/paste from django/template/base.py in
+        # Variable._resolve_lookup()
+        current = context
+        for bit in self.bits:
+            try:  # dictionary lookup
+                current = current[bit]
+            except (TypeError, AttributeError, KeyError):
+                try:  # attribute lookup
+                    current = getattr(current, bit)
+                except (TypeError, AttributeError):
+                    try:  # list-index lookup
+                        current = current[int(bit)]
+                    except (IndexError, # list index out of range
+                            ValueError, # invalid literal for int()
+                            KeyError,   # dict without `int(bit)` key
+                            TypeError,  # unsubscriptable object
+                            ):
+                        raise ValueError('Failed lookup for key [%s] in %r'
+                                         ', when resolving the accessor %s'
+                                          % (bit, current, self))
+            if callable(current):
+                current = current()
+            # important that we break in None case, or a relationship
+            # spanning across a null-key will raise an exception in the
+            # next iteration, instead of defaulting.
+            if current is None:
+                break
+        return current
 
     @property
     def bits(self):
-        return self.path.split(self.SEPARATOR)
+        return self.split(self.SEPARATOR)
+
 
+A = Accessor  # alias
 
 class AttributeDict(dict):
     """A wrapper around :class:`dict` that knows how to render itself as HTML