Column defaults are now more sensible, resolves #64
This commit is contained in:
parent
2db92e421b
commit
315583bec5
|
@ -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
|
||||
-------
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# coding: utf-8
|
||||
from .tables import Table
|
||||
from .columns import (Column, CheckBoxColumn, LinkColumn, TemplateColumn,
|
||||
EmailColumn, URLColumn)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# coding: utf-8
|
||||
|
||||
|
||||
class RequestConfig(object):
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# coding: utf-8
|
||||
"""Needed to make this package a Django app"""
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue