Column defaults are now more sensible, resolves #64

This commit is contained in:
Bradley Ayers 2012-06-24 10:48:41 +10:00
parent 2db92e421b
commit 315583bec5
14 changed files with 132 additions and 81 deletions

View File

@ -63,6 +63,14 @@ globally, use::
Change log
==========
v0.12.0
-------
- `Column.render` is now skipped if the value is considered *empty*, the
default value is used instead. Empty values are specified via
`Column.empty_values`, by default is `(None, '')`. (backwards incompatible)
- Default values can now be specified on table instances or `Table.Meta`.
v0.11.0
-------

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8
from .tables import Table
from .columns import (Column, CheckBoxColumn, LinkColumn, TemplateColumn,
EmailColumn, URLColumn)

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8
from __future__ import absolute_import, unicode_literals
from django.core.urlresolvers import reverse
from django.db.models.fields import FieldDoesNotExist
@ -71,10 +71,11 @@ class Column(object): # pylint: disable=R0902
"""
#: Tracks each time a Column instance is created. Used to retain order.
creation_counter = 0
empty_values = (None, '')
def __init__(self, verbose_name=None, accessor=None, default=None,
visible=True, orderable=None, attrs=None, order_by=None,
sortable=None):
sortable=None, empty_values=None):
if not (accessor is None or isinstance(accessor, basestring) or
callable(accessor)):
raise TypeError('accessor must be a string or callable, not %s' %
@ -101,18 +102,15 @@ class Column(object): # pylint: disable=R0902
# massage order_by into an OrderByTuple or None
order_by = (order_by, ) if isinstance(order_by, basestring) else order_by
self.order_by = OrderByTuple(order_by) if order_by is not None else None
if empty_values is not None:
self.empty_values = empty_values
self.creation_counter = Column.creation_counter
Column.creation_counter += 1
@property
def default(self):
"""
The default value for cells in this column.
The default value passed into ``Column.default`` property may be a
callable, this function handles access.
"""
# handle callables
return self._default() if callable(self._default) else self._default
@property
@ -147,6 +145,13 @@ class Column(object): # pylint: disable=R0902
This method can be overridden by :meth:`render_FOO` methods on the
table or by subclassing :class:`Column`.
:returns: `unicode`
If the value for this cell is in `self.empty_values`, this method is
skipped and an appropriate default value is rendered instead.
Subclasses should set `empty_values` to `()` if they want to handle
all values in `render`.
"""
return value
@ -325,16 +330,6 @@ class LinkColumn(BaseLinkColumn):
self.current_app = current_app
def render(self, value, record, bound_column): # pylint: disable=W0221
# Remember that value is actually what would have normally been put
# into the cell. i.e. it *already* takes into consideration the
# column's *default* property, thus we must check the actual data value
# and use that to decide whether to render a link or just the default
try:
raw = bound_column.accessor.resolve(record)
except (TypeError, AttributeError, KeyError, ValueError):
raw = None
if raw is None:
return self.default
# The following params + if statements create the arguments required to
# pass to Django's reverse() function.
params = {}
@ -452,6 +447,8 @@ class TemplateColumn(Column):
``RequestContext``, the table **must** be rendered via
:ref:`{% render_table %} <template-tags.render_table>`.
"""
empty_values = ()
def __init__(self, template_code=None, template_name=None, **extra):
super(TemplateColumn, self).__init__(**extra)
self.template_code = template_code
@ -500,9 +497,9 @@ class BoundColumn(object):
"""
def __init__(self, table, column, name):
self._table = table
self._column = column
self._name = name
self.table = table
self.column = column
self.name = name
def __unicode__(self):
return unicode(self.header)
@ -549,19 +546,15 @@ class BoundColumn(object):
td['class'] = " ".join(sorted(td_class))
return attrs
@property
def column(self):
"""
Returns the :class:`.Column` object for this column.
"""
return self._column
@property
def default(self):
"""
Returns the default value for this column.
"""
return self.column.default
value = self.column.default
if value is None:
value = self.table.default
return value
@property
def header(self):
@ -581,13 +574,6 @@ class BoundColumn(object):
return verbose_name
return title(verbose_name)
@property
def name(self):
"""
Returns the string used to identify this column.
"""
return self._name
@property
def order_by(self):
"""
@ -680,13 +666,6 @@ class BoundColumn(object):
return self.column.orderable
return self.table.orderable
@property
def table(self):
"""
Return the :class:`Table` object that this column is part of.
"""
return self._table
@property
def verbose_name(self):
"""

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8
class RequestConfig(object):

View File

@ -1,2 +1,2 @@
# -*- coding: utf-8 -*-
# coding: utf-8
"""Needed to make this package a Django app"""

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8
from itertools import imap
from django.db import models
from django.db.models.fields import FieldDoesNotExist
@ -102,33 +102,35 @@ class BoundRow(object):
"""
bound_column = self.table.columns[name]
def value():
try:
# We need to take special care here to allow get_FOO_display()
# methods on a model to be used if available. See issue #30.
path, _, remainder = bound_column.accessor.rpartition('.')
penultimate = A(path).resolve(self.record)
# If the penultimate is a model and the remainder is a field
# using choices, use get_FOO_display().
if isinstance(penultimate, models.Model):
try:
field = penultimate._meta.get_field(remainder)
display = getattr(penultimate, 'get_%s_display' % remainder, None)
if field.choices and display:
raw = display()
remainder = None
except FieldDoesNotExist:
pass
# Fall back to just using the original accessor (we just need
# to follow the remainder).
if remainder:
raw = A(remainder).resolve(penultimate)
except (TypeError, AttributeError, KeyError, ValueError):
raw = None
return raw if raw is not None else bound_column.default
value = None
try:
# We need to take special care here to allow get_FOO_display()
# methods on a model to be used if available. See issue #30.
path, _, remainder = bound_column.accessor.rpartition('.')
penultimate = A(path).resolve(self.record)
# If the penultimate is a model and the remainder is a field
# using choices, use get_FOO_display().
if isinstance(penultimate, models.Model):
try:
field = penultimate._meta.get_field(remainder)
display = getattr(penultimate, 'get_%s_display' % remainder, None)
if field.choices and display:
value = display()
remainder = None
except FieldDoesNotExist:
pass
# Fall back to just using the original accessor (we just need
# to follow the remainder).
if remainder:
value = A(remainder).resolve(penultimate)
except (TypeError, AttributeError, KeyError, ValueError):
pass
if value in bound_column.column.empty_values:
return bound_column.default
kwargs = {
'value': value, # already a function, no need to wrap
'value': lambda: value,
'record': lambda: self.record,
'column': lambda: bound_column.column,
'bound_column': lambda: bound_column,

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8
import copy
from django.conf import settings
from django.core.paginator import Paginator
@ -155,6 +155,7 @@ class TableOptions(object):
def __init__(self, options=None):
super(TableOptions, self).__init__()
self.attrs = AttributeDict(getattr(options, "attrs", {}))
self.default = getattr(options, "default", u"")
self.empty_text = getattr(options, "empty_text", None)
self.fields = getattr(options, "fields", ())
self.exclude = getattr(options, "exclude", ())
@ -230,6 +231,10 @@ class Table(StrAndUnicode):
:type empty_text: string
:param empty_text: Empty text to render when the table has no data.
(default :attr:`.Table.Meta.empty_text`)
:type default: unicode
:param default: Text to render in empty cells (determined by
:attr:`Column.empty_values`, default :attrs:`.Table.Meta.default`)
"""
__metaclass__ = DeclarativeColumnsMetaclass
TableDataClass = TableData
@ -237,11 +242,14 @@ class Table(StrAndUnicode):
def __init__(self, data, order_by=None, orderable=None, empty_text=None,
exclude=None, attrs=None, sequence=None, prefix=None,
order_by_field=None, page_field=None, per_page_field=None,
template=None, sortable=None):
template=None, sortable=None, default=None):
super(Table, self).__init__()
self.exclude = exclude or ()
self.sequence = sequence
self.data = self.TableDataClass(data=data, table=self)
if default is None:
default = self._meta.default
self.default = default
self.rows = BoundRows(self.data)
self.attrs = attrs
self.empty_text = empty_text

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8
from __future__ import absolute_import
from django import template
from django.template import TemplateSyntaxError, Variable, Node

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8
from __future__ import absolute_import, unicode_literals
from django.utils.html import escape
from django.utils.safestring import mark_safe

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding: utf-8
from django.core.exceptions import ImproperlyConfigured
from django.views.generic.list import ListView
from .config import RequestConfig

View File

@ -345,6 +345,14 @@ def th_are_given_sortable_class_if_column_is_orderable():
assert "sortable" not in classes(root.findall('.//thead/tr/th')[1])
@general.test
def empty_values_triggers_default():
class Table(tables.Table):
a = tables.Column(empty_values=(1, 2), default="--")
table = Table([{"a": 1}, {"a": 2}, {"a": 3}, {"a": 4}])
assert [x["a"] for x in table.rows] == ["--", "--", 3, 4]
linkcolumn = Tests()
linkcolumn.context(TestContext())
@ -428,6 +436,15 @@ def a_attrs_should_be_supported():
"title": "Occupation Title"}
@linkcolumn.test
def defaults():
class Table(tables.Table):
link = tables.LinkColumn('occupation', kwargs={"pk": 1}, default="xyz")
table = Table([{}])
assert table.rows[0]['link'] == 'xyz'
templatecolumn = Tests()

View File

@ -254,7 +254,7 @@ def ordering_different_types():
]
table = OrderedTable(data)
assert None == table.rows[0]['alpha']
assert u"" == table.rows[0]['alpha']
table = OrderedTable(data, order_by='i')
assert 1 == table.rows[0]['i']
@ -544,3 +544,40 @@ def should_support_rendering_multiple_times():
# test list data
table = MultiRenderTable([{'name': 'brad'}])
assert table.as_html() == table.as_html()
@core.test
def column_defaults_are_honored():
class Table(tables.Table):
name = tables.Column(default="abcd")
class Meta:
default = "efgh"
table = Table([{}], default="ijkl")
assert table.rows[0]['name'] == "abcd"
@core.test
def table_meta_defaults_are_honored():
class Table(tables.Table):
name = tables.Column()
class Meta:
default = "abcd"
table = Table([{}])
assert table.rows[0]['name'] == "abcd"
@core.test
def table_defaults_are_honored():
class Table(tables.Table):
name = tables.Column()
table = Table([{}], default="abcd")
assert table.rows[0]['name'] == "abcd"
table = Table([{}], default="abcd")
table.default = "efgh"
assert table.rows[0]['name'] == "efgh"

View File

@ -4,7 +4,7 @@ from django.conf import settings
from django.template import Template, Context
import django_tables2 as tables
from django_attest import TestContext
from attest import Tests, Assert
from attest import assert_hook, Tests
from .app.models import Person, Occupation

View File

@ -96,8 +96,8 @@ def custom_rendering():
# row values
template = Template('{% for row in countries.rows %}{% for value in row %}'
'{{ value }} {% endfor %}{% endfor %}')
result = ('Germany Berlin 83 49 France None 64 33 Netherlands Amsterdam '
'None 31 Austria None 8 43 ')
result = (u'Germany Berlin 83 49 France 64 33 Netherlands Amsterdam '
u' 31 Austria 8 43 ')
assert result == template.render(context)