From b97888c9b83240e0d61f660b984b1f977e3c8933 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Fri, 1 Apr 2011 17:50:56 +1000 Subject: [PATCH] * Fixed CheckBoxColumn. * Fixed rendering of column headers. SafeData is now honored. * Added LinkColumn. --- django_tables/columns.py | 138 +++++++++++------- django_tables/rows.py | 43 ++++-- django_tables/tables.py | 17 --- .../templates/django_tables/table.html | 4 +- django_tables/utils.py | 70 +++++---- 5 files changed, 155 insertions(+), 117 deletions(-) diff --git a/django_tables/columns.py b/django_tables/columns.py index 5e7f82a..05514be 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -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 ```` """ - 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('') + + def render(self, value, bound_column, **kwargs): + attrs = AttributeDict({ + 'type': 'checkbox', + 'name': bound_column.name, + 'value': value + }) attrs.update(self.attrs) - t = Template('') - return t.render(Context({ - 'value': self.value(bound_column=bound_column, - bound_row=bound_row), - 'attrs': attrs, - })) - - -class BoundColumn(StrAndUnicode): + return mark_safe('' % 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 ```` 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 = '{value}'.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): diff --git a/django_tables/rows.py b/django_tables/rows.py index a6f9ed9..d7e9e4a 100644 --- a/django_tables/rows.py +++ b/django_tables/rows.py @@ -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.""" diff --git a/django_tables/tables.py b/django_tables/tables.py index a65d7f7..3e8afdf 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -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] diff --git a/django_tables/templates/django_tables/table.html b/django_tables/templates/django_tables/table.html index 2681a2a..7212876 100644 --- a/django_tables/templates/django_tables/table.html +++ b/django_tables/templates/django_tables/table.html @@ -6,10 +6,10 @@ {% for column in table.columns %} {% if column.sortable %} {% with column.order_by as ob %} - {{ column }} + {{ column.verbose_name }} {% endwith %} {% else %} - {{ column }} + {{ column.verbose_name }} {% endif %} {% endfor %} diff --git a/django_tables/utils.py b/django_tables/utils.py index 2a6bd83..f327df2 100644 --- a/django_tables/utils.py +++ b/django_tables/utils.py @@ -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 -- 2.26.2