# -*- 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):
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:
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)):
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
"""
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
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):
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."""
@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):
# -*- 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:
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."""
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