diff --git a/README b/README
deleted file mode 100644
index 2c28c878..00000000
--- a/README
+++ /dev/null
@@ -1,4 +0,0 @@
-django-tables - a Django QuerySet renderer.
-
-Documentation:
- http://elsdoerfer.name/docs/django-tables/
diff --git a/README.rst b/README.rst
new file mode 100644
index 00000000..7dbbd1aa
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,67 @@
+==================================
+DataTabler - Easy Tables in Django
+==================================
+
+:Version: 0.1.0dev
+:Web: http://github.com/winhamwr/django-tables/
+:Source: http://github.com/winhamwr/django-tables/
+
+.. datatabler-synopsis:
+
+DataTabler is an easy way to turn your Django Querysets, dictionary lists or
+other structured data in to an HTML table with server-side sorting and
+consistent formatting. Define the data relationship you want to display in one
+place, and then DRY to table victory.
+
+DataTabler was derived from `django-tables`_ and is a very similar library.
+
+.. datatabler-overview:
+
+Overview
+========
+
+Dealing with sorting, consistent formatting and derived fields when deplaying
+querysets and other structured data can be a pain. How much do you put in
+your view, versus the template and how much should you try to DRY with include
+templates and templatetags? What about creating CRUD links, but only for users
+with permissions on the individual objects?
+
+With DataTabler, you define the columns you want to expose in the table up
+front, along with sensible names, free sorting and the power of python for
+DRYing up any non-display logic.
+
+.. datatabler-example:
+
+Example
+=======
+
+Let's say you'd like to display a table of documents in a document management
+system. You need an edit link, author (sortable by last name) and a preview
+of the document. On another page you don't need the preview anymore, but you
+need to be able to . Your Document model looks like this:
+::
+
+ # models.py
+ class Document(models.Model):
+ name = models.CharField(max_length=200)
+ html = models.TextField(blank=True)
+ author = models.ForeignKey(User, related_name='documents_authored_set')
+
+
+With DataTabler, you'd define a ``Table`` (anywhere, but tables.py is nice) like
+so:
+::
+
+ # tables.py
+ class DocumentTable(datatabler.ModelTable):
+ edit_link = Column(sortable=False, verbose_name=' ')
+::
+
+ # view.py
+ def list_documents(request):
+
+License
+=======
+
+This software is licensed under the `New BSD License`. See the ``LICENSE``
+file in the top distribution directory for the full license text.
diff --git a/django_tables/base.py b/django_tables/base.py
index 75b5e954..5b5771d1 100644
--- a/django_tables/base.py
+++ b/django_tables/base.py
@@ -190,12 +190,10 @@ def _spawn_columns(self):
# BoundColumn instances can be costly, so we reuse existing ones.
new_columns = SortedDict()
for decl_name, column in self.table.base_columns.items():
- # take into account name overrides
- exposed_name = column.name or decl_name
- if exposed_name in self._columns:
- new_columns[exposed_name] = self._columns[exposed_name]
+ if decl_name in self._columns:
+ new_columns[decl_name] = self._columns[decl_name]
else:
- new_columns[exposed_name] = BoundColumn(self.table, column, decl_name)
+ new_columns[decl_name] = BoundColumn(self.table, column, decl_name)
self._columns = new_columns
def all(self):
@@ -262,28 +260,28 @@ def __getitem__(self, name):
class BoundColumn(StrAndUnicode):
- """'Runtime' version of ``Column`` that is bound to a table instance,
+ """
+ 'Runtime' version of ``Column`` that is bound to a table instance,
and thus knows about the table's data.
-
- Note that the name that is passed in tells us how this field is
- delared in the bound table. The column itself can overwrite this name.
- While the overwritten name will be hat mostly counts, we need to
- remember the one used for declaration as well, or we won't know how
- to read a column's value from the source.
"""
- def __init__(self, table, column, name):
+ def __init__(self, table, column, declared_name):
self.table = table
self.column = column
- self.declared_name = name
+ self.declared_name = declared_name
# expose some attributes of the column more directly
- self.visible = column.visible
+ self.visible = False
+ if column:
+ self.visible = column.visible
@property
- def accessor(self):
- """The key to use when accessing this column's values in the
+ def src_accessor(self):
+ """
+ The key to use when accessing this column's values in the
source data.
"""
- return self.column.data if self.column.data else self.declared_name
+ if self.column.model_rel:
+ return self.column.model_rel
+ return self.declared_name
def _get_sortable(self):
if self.column.sortable is not None:
@@ -294,7 +292,7 @@ def _get_sortable(self):
return True # the default value
sortable = property(_get_sortable)
- name = property(lambda s: s.column.name or s.declared_name)
+ name = property(lambda s: s.declared_name)
name_reversed = property(lambda s: "-"+s.name)
def _get_name_toggled(self):
o = self.table.order_by
@@ -320,11 +318,6 @@ def get_default(self, row):
return self.column.default(row)
return self.column.default
- def _get_values(self):
- # TODO: build a list of values used
- pass
- values = property(_get_values)
-
def __unicode__(self):
s = self.column.verbose_name or self.name.replace('_', ' ')
return capfirst(force_unicode(s))
@@ -334,7 +327,8 @@ def as_html(self):
class BoundRow(object):
- """Represents a single row of data, bound to a table.
+ """
+ Represents a single row of data, bound to a table.
Tables will spawn these row objects, wrapping around the actual data
stored in a row.
@@ -348,9 +342,10 @@ def __iter__(self):
yield value
def __getitem__(self, name):
- """Returns this row's value for a column. All other access methods,
- e.g. __iter__, lead ultimately to this."""
-
+ """
+ Returns this row's value for a column. All other access methods,
+ e.g. __iter__, lead ultimately to this.
+ """
column = self.table.columns[name]
render_func = getattr(self.table, 'render_%s' % name, False)
@@ -360,10 +355,11 @@ def __getitem__(self, name):
return self._default_render(column)
def _default_render(self, column):
- """Returns a cell's content. This is used unless the user
+ """
+ Returns a cell's content. This is used unless the user
provides a custom ``render_FOO`` method.
"""
- result = self.data[column.accessor]
+ result = self.data[column.src_accessor]
# if the field we are pointing to is a callable, remove it
if callable(result):
@@ -371,7 +367,9 @@ def _default_render(self, column):
return result
def __contains__(self, item):
- """Check by both row object and column name."""
+ """
+ Check by both row object and column name.
+ """
if isinstance(item, basestring):
return item in self.table._columns
else:
@@ -387,7 +385,8 @@ def as_html(self):
class Rows(object):
- """Container for spawning BoundRows.
+ """
+ Container for spawning BoundRows.
This is bound to a table and provides it's ``rows`` property. It
provides functionality that would not be possible with a simple
@@ -403,12 +402,16 @@ def _reset(self):
pass # we currently don't use a cache
def all(self):
- """Return all rows."""
+ """
+ Return all rows.
+ """
for row in self.table.data:
yield self.row_class(self.table, row)
def page(self):
- """Return rows on current page (if paginated)."""
+ """
+ Return rows on current page (if paginated).
+ """
if not hasattr(self.table, 'page'):
return None
return iter(self.table.page.object_list)
@@ -432,7 +435,8 @@ def __getitem__(self, key):
class BaseTable(object):
- """A collection of columns, plus their associated data rows.
+ """
+ A collection of columns, plus their associated data rows.
"""
__metaclass__ = DeclarativeColumnsMetaclass
@@ -445,7 +449,8 @@ class BaseTable(object):
DefaultOrder = type('DefaultSortType', (), {})()
def __init__(self, data, order_by=DefaultOrder):
- """Create a new table instance with the iterable ``data``.
+ """
+ Create a new table instance with the iterable ``data``.
If ``order_by`` is specified, the data will be sorted accordingly.
Otherwise, the sort order can be specified in the table options.
@@ -481,7 +486,8 @@ def __init__(self, data, order_by=DefaultOrder):
self.base_columns = copy.deepcopy(type(self).base_columns)
def _reset_snapshot(self, reason):
- """Called to reset the current snaptshot, for example when
+ """
+ Called to reset the current snaptshot, for example when
options change that could affect it.
``reason`` is given so that subclasses can decide that a
@@ -490,7 +496,8 @@ def _reset_snapshot(self, reason):
self._snapshot = None
def _build_snapshot(self):
- """Rebuild the table for the current set of options.
+ """
+ Rebuild the table for the current set of options.
Whenver the table options change, e.g. say a new sort order,
this method will be asked to regenerate the actual table from
@@ -507,7 +514,8 @@ def _get_data(self):
data = property(lambda s: s._get_data())
def _resolve_sort_directions(self, order_by):
- """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes
+ """
+ Given an ``order_by`` tuple, this will toggle the hyphen-prefixes
according to each column's ``direction`` option, e.g. it translates
between the ascending/descending and the straight/reverse terminology.
"""
@@ -518,22 +526,22 @@ def _resolve_sort_directions(self, order_by):
result.append(inst)
return result
- def _cols_to_fields(self, names):
- """Utility function. Given a list of column names (as exposed to
+ def _col_names_to_src_names(self, names):
+ """
+ Utility function. Given a list of column names (as exposed to
the user), converts column names to the names we have to use to
retrieve a column's data from the source.
Usually, the name used in the table declaration is used for accessing
- the source (while a column can define an alias-like name that will
- be used to refer to it from the "outside"). However, a column can
- override this by giving a specific source field name via ``data``.
+ the source. However, a column can override this by giving a specific
+ source field name via ``model_rel``.
Supports prefixed column names as used e.g. in order_by ("-field").
"""
- result = []
+ src_names = []
for ident in names:
# handle order prefix
- if ident[:1] == '-':
+ if ident.startswith('-'):
name = ident[1:]
prefix = '-'
else:
@@ -541,11 +549,12 @@ def _cols_to_fields(self, names):
prefix = ''
# find the field name
column = self.columns[name]
- result.append(prefix + column.accessor)
- return result
+ src_names.append(prefix + column.src_accessor)
+ return src_names
def _validate_column_name(self, name, purpose):
- """Return True/False, depending on whether the column ``name`` is
+ """
+ Return True/False, depending on whether the column ``name`` is
valid for ``purpose``. Used to validate things like ``order_by``
instructions.
diff --git a/django_tables/columns.py b/django_tables/columns.py
index 3307f092..9124640b 100644
--- a/django_tables/columns.py
+++ b/django_tables/columns.py
@@ -7,11 +7,11 @@ class Column(object):
``verbose_name`` defines a display name for this column used for output.
- ``name`` is the internal name of the column. Normally you don't need to
- specify this, as the attribute that you make the column available under
- is used. However, in certain circumstances it can be useful to override
- this default, e.g. when using ModelTables if you want a column to not
- use the model field name.
+ ``model_rel`` is the Django ORM-style name of the relationship path from
+ your Model to the field represented by this column. By default, the
+ column"s label is assumed to be the path to the field, but in cases where
+ two different columns are both backed by the same field, you can use
+ the same model_rel on two columns.
``default`` is the default value for this column. If the data source
does provide ``None`` for a row, the default will be used instead. Note
@@ -19,15 +19,6 @@ class Column(object):
or normal). Also, you can specify a callable, which will be passed a
``BoundRow`` instance and is expected to return the default to be used.
- Additionally, you may specify ``data``. It works very much like
- ``default``, except it's effect does not depend on the actual cell
- value. When given a function, it will always be called with a row object,
- expected to return the cell value. If given a string, that name will be
- used to read the data from the source (instead of the column's name).
-
- Note the interaction with ``default``. If ``default`` is specified as
- well, it will be used whenver ``data`` yields in a None value.
-
You can use ``visible`` to flag the column as hidden by default.
However, this can be overridden by the ``visibility`` argument to the
table constructor. If you want to make the column completely unavailable
@@ -46,18 +37,12 @@ class Column(object):
# Tracks each time a Column instance is created. Used to retain order.
creation_counter = 0
- def __init__(self, verbose_name=None, name=None, default=None, data=None,
+ def __init__(self, verbose_name=None, model_rel=None, default=None,
visible=True, inaccessible=False, sortable=None,
direction=ASC):
self.verbose_name = verbose_name
- self.name = name
+ self.model_rel = model_rel
self.default = default
- self.data = data
- if callable(self.data):
- raise DeprecationWarning(('The Column "data" argument may no '+
- 'longer be a callable. Add a '+
- '``render_%s`` method to your '+
- 'table instead.') % (name or 'FOO'))
self.visible = visible
self.inaccessible = inaccessible
self.sortable = sortable
diff --git a/django_tables/memory.py b/django_tables/memory.py
index 40d147cc..a4b93d84 100644
--- a/django_tables/memory.py
+++ b/django_tables/memory.py
@@ -47,38 +47,28 @@ def _build_snapshot(self):
# probably much better to do this on-demand instead, when the
# data is *needed* for the first time.
"""
-
# reset caches
self._columns._reset()
self._rows._reset()
snapshot = copy.copy(self._data)
- for row in snapshot:
- # add data that is missing from the source. we do this now so
- # that the colunn ``default`` and ``data`` values can affect
- # sorting (even when callables are used)!
+ # Fill in ``default`` values where needed
+ for src_row in snapshot:
+ # We do this now so that column ``default`` values can affect
+ # sorting, even with callables
# This is a design decision - the alternative would be to
# resolve the values when they are accessed, and either do not
# support sorting them at all, or run the callables during
# sorting.
for column in self.columns.all():
- name_in_source = column.declared_name
- if column.column.data:
- if callable(column.column.data):
- # if data is a callable, use it's return value
- row[name_in_source] = column.column.data(BoundRow(self, row))
- else:
- name_in_source = column.column.data
-
- # the following will be True if:
- # * the source does not provide that column or provides None
- # * the column did provide a data callable that returned None
- if row.get(name_in_source, None) is None:
- row[name_in_source] = column.get_default(BoundRow(self, row))
+ if src_row.get(column.src_accessor, None) is None:
+ # No value was provided in the source, so use the default
+ src_row[column.src_accessor] = column.get_default(
+ BoundRow(self, src_row))
if self.order_by:
actual_order_by = self._resolve_sort_directions(self.order_by)
- sort_table(snapshot, self._cols_to_fields(actual_order_by))
+ sort_table(snapshot, self._col_names_to_src_names(actual_order_by))
return snapshot
diff --git a/django_tables/models.py b/django_tables/models.py
index b3ccb097..153ea6ca 100644
--- a/django_tables/models.py
+++ b/django_tables/models.py
@@ -55,11 +55,12 @@ class BoundModelRow(BoundRow):
"""
def _default_render(self, boundcol):
- """In the case of a model table, the accessor may use ``__`` to
+ """
+ In the case of a model table, the accessor may use ``__`` to
span instances. We need to resolve this.
"""
# try to resolve relationships spanning attributes
- bits = boundcol.accessor.split('__')
+ bits = boundcol.src_accessor.split('__')
current = self.data
for bit in bits:
# note the difference between the attribute being None and not
@@ -70,7 +71,7 @@ def _default_render(self, boundcol):
# also ``_validate_column_name``, where such a mechanism is
# already implemented).
if not hasattr(current, bit):
- raise ValueError("Could not resolve %s from %s" % (bit, boundcol.accessor))
+ raise ValueError("Could not resolve %s from %s" % (bit, boundcol.src_accessor))
current = getattr(current, bit)
if callable(current):
@@ -148,17 +149,18 @@ class Meta:
just don't any data at all, the model the table is based on will
provide it.
"""
-
__metaclass__ = ModelTableMetaclass
rows_class = ModelRows
def __init__(self, data=None, *args, **kwargs):
- if data == None:
+ if not data:
+ data = None
+ if data is None:
if self._meta.model is None:
raise ValueError('Table without a model association needs '
'to be initialized with data')
- self.queryset = self._meta.model._default_manager.all()
+ self.queryset = self._meta.model._default_manager.none()
elif hasattr(data, '_default_manager'): # saves us db.models import
self.queryset = data._default_manager.all()
else:
@@ -167,9 +169,10 @@ def __init__(self, data=None, *args, **kwargs):
super(ModelTable, self).__init__(self.queryset, *args, **kwargs)
def _validate_column_name(self, name, purpose):
- """Overridden. Only allow model-based fields and valid model
- spanning relationships to be sorted."""
-
+ """
+ Overridden. Only allow model-based fields and valid model
+ spanning relationships to be sorted.
+ """
# let the base class sort out the easy ones
result = super(ModelTable, self)._validate_column_name(name, purpose)
if not result:
@@ -178,51 +181,37 @@ def _validate_column_name(self, name, purpose):
if purpose == 'order_by':
column = self.columns[name]
- # "data" can really be used in two different ways. It is
- # slightly confusing and potentially should be changed.
- # It can either refer to an attribute/field which the table
- # column should represent, or can be a callable (or a string
- # pointing to a callable attribute) that is used to render to
- # cell. The difference is that in the latter case, there may
- # still be an actual source model field behind the column,
- # stored in "declared_name". In other words, we want to filter
- # out column names that are not oderable, and the column name
- # we need to check may either be stored in "data" or in
- # "declared_name", depending on if and what kind of value is
- # in "data". This is the reason why we try twice.
- #
- # See also bug #282964.
- #
# TODO: It might be faster to try to resolve the given name
# manually recursing the model metadata rather than
# constructing a queryset.
- for lookup in (column.column.data, column.declared_name):
- if not lookup or callable(lookup):
- continue
- try:
- # Let Django validate the lookup by asking it to build
- # the final query; the way to do this has changed in
- # Django 1.2, and we try to support both versions.
- _temp = self.queryset.order_by(lookup).query
- if hasattr(_temp, 'as_sql'):
- _temp.as_sql()
- else:
- from django.db import DEFAULT_DB_ALIAS
- _temp.get_compiler(DEFAULT_DB_ALIAS).as_sql()
- break
- except FieldError:
- pass
- else:
+ try:
+ # Let Django validate the lookup by asking it to build
+ # the final query; the way to do this has changed in
+ # Django 1.2, and we try to support both versions.
+
+ # Using the model._default_manager to get a standard manager
+ # in case we're sorting on a "fake" queryset that doesn't
+ # implement the SQL compiler
+ _temp = self.queryset.model._default_manager.order_by(
+ column.src_accessor).query
+ if hasattr(_temp, 'as_sql'):
+ _temp.as_sql()
+ else:
+ from django.db import DEFAULT_DB_ALIAS
+ _temp.get_compiler(DEFAULT_DB_ALIAS).as_sql()
+ except FieldError:
return False
+ else:
+ return False
# if we haven't failed by now, the column should be valid
return True
def _build_snapshot(self):
- """Overridden. The snapshot in this case is simply a queryset
+ """
+ Overridden. The snapshot in this case is simply a queryset
with the necessary filters etc. attached.
"""
-
# reset caches
self._columns._reset()
self._rows._reset()
@@ -230,5 +219,7 @@ def _build_snapshot(self):
queryset = self.queryset
if self.order_by:
actual_order_by = self._resolve_sort_directions(self.order_by)
- queryset = queryset.order_by(*self._cols_to_fields(actual_order_by))
+ queryset = queryset.order_by(
+ *self._col_names_to_src_names(actual_order_by))
+
return queryset
diff --git a/django_tables/utils.py b/django_tables/utils.py
new file mode 100644
index 00000000..4a478c94
--- /dev/null
+++ b/django_tables/utils.py
@@ -0,0 +1,10 @@
+def get_order_by(query_dict, order_by_param, secondary=None):
+ '''
+ ``query_dict`` is either the get or post data
+ ``order_by_param`` is the variable name with which to sort on
+ ``default`` which column to order on in a default case
+ '''
+ order_by = query_dict.get(order_by_param)
+ if order_by and not order_by.lstrip('-') == secondary:
+ order_by = [order_by, secondary]
+ return order_by
diff --git a/docs/columns.rst b/docs/columns.rst
index e9c7a1af..202ce26f 100644
--- a/docs/columns.rst
+++ b/docs/columns.rst
@@ -16,39 +16,33 @@ There are no required arguments. The following is fine:
.. code-block:: python
class MyTable(tables.MemoryTable):
- c = tables.Column()
+ count = tables.Column()
-It will result in a column named ``c`` in the table. You can specify the
-``name`` to override this:
+It will result in a column named ``count`` in the table that references the
+value ``count`` in your source data (if present). . You can specify the
+``model_rel`` to override the source data this field is referencing :
.. code-block:: python
- c = tables.Column(name="count")
+ count = tables.Column(model_rel="src_count")
-The column is now called and accessed via "count", although the table will
-still use ``c`` to read it's values from the source. You can however modify
-that as well, by specifying ``data``:
+The column is still called and accessed via "count", but now the table will
+ use ``src_count`` to read it's values from the source.
-.. code-block:: python
-
- c = tables.Column(name="count", data="count")
-
-For practicual purposes, ``c`` is now meaningless. While in most cases
-you will just define your column using the name you want it to have, the
-above is useful when working with columns automatically generated from
-models:
+This is useful for occasions where you'd like to place meaningful names mapping
+to input fields with less-useful names.
.. code-block:: python
class BookTable(tables.ModelTable):
- book_name = tables.Column(name="name")
- author = tables.Column(data="info__author__name")
+ book_name = tables.Column(model_rel="name")
+ author_name = tables.Column(model_rel="info__author__name")
class Meta:
model = Book
-The overwritten ``book_name`` field/column will now be exposed as the
-cleaner ``name``, and the new ``author`` column retrieves it's values from
-``Book.info.author.name``.
+The overwritten ``Book.name`` field will now be exposed via the table as
+the ``book_name`` column, and the new ``author_name`` column retrieves it's
+values from ``Book.info.author.name``.
Apart from their internal name, you can define a string that will be used
when for display via ``verbose_name``:
diff --git a/docs/index.rst b/docs/index.rst
index a79fb6e8..68815780 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -23,10 +23,15 @@ The API looks similar to that of Django's ``ModelForms``:
import django_tables as tables
+ MY_TIMEZONE = -5
class CountryTable(tables.MemoryTable):
name = tables.Column(verbose_name="Country Name")
population = tables.Column(sortable=False, visible=False)
- time_zone = tables.Column(name="tz", default="UTC+1")
+ time_zone = tables.Column(model_rel="tz", default="+1")
+ time_diff = tables.Column(model_rel="tz")
+
+ def render_time_diff(self, country):
+ return int(country.tz) - MY_TIMEZONE
Instead of fields, you declare a column for every piece of data you want
to expose to the user.
@@ -79,15 +84,14 @@ template:
def list_countries(request):
data = ...
- countries = CountryTable(data, order_by=request.GET.get('sort'))
+ countries = CountryTable(data, order_by=request.GET.get('sort', 'name'))
return render_to_response('list.html', {'table': countries})
Note that we are giving the incoming ``sort`` query string value directly to
the table, asking for a sort. All invalid column names will (by default) be
-ignored. In this example, only ``name`` and ``tz`` are allowed, since:
+ignored. In this example, only ``name`` and ``time_zone`` are allowed, since:
* ``population`` has ``sortable=False``
- * ``time_zone`` has it's name overwritten with ``tz``.
Then, in the ``list.html`` template, write:
@@ -114,13 +118,13 @@ which in turn will be used by the server for ordering. ``order_by`` accepts
comma-separated strings as input, and ``{{ column.name_toggled }}`` will be
rendered as a such a string.
-Instead of the iterator, you can alos use your knowledge of the table
+Instead of the iterator, you can also use your knowledge of the table
structure to access columns directly:
.. code-block:: django
- {% if table.columns.tz.visible %}
- {{ table.columns.tz }}
+ {% if table.columns.time_zone.visible %}
+ {{ table.columns.time_zone }}
{% endfor %}
diff --git a/docs/templates.rst b/docs/templates.rst
index c3ee8bb7..da683b36 100644
--- a/docs/templates.rst
+++ b/docs/templates.rst
@@ -47,23 +47,23 @@ your Python code:
class BookTable(tables.ModelTable):
name = tables.Column()
- rating_int = tables.Column(name="rating")
+ rating_int = tables.Column(model_rel="rating_count")
- def render_rating(self, instance):
- if instance.rating_count == 0:
+ def render_rating(self, book):
+ if book.rating_count == 0:
return '
'
else:
- return '
' % instance.rating_int
+ return '
' % book.rating_count
-When accessing ``table.rows[i].rating``, the ``render_rating`` method
+When accessing ``table.rows[i].rating_int``, the ``render_rating_int`` method
will be called. Note the following:
- What is passed is underlying raw data object, in this case, the
model instance. This gives you access to data values that may not
have been defined as a column.
- For the method name, the public name of the column must be used, not
- the internal field name. That is, it's ``render_rating``, not
- ``render_rating_int``.
+ the internal field name. That is, it's ``render_rating_int``, not
+ ``render_rating``.
- The method is called whenever the cell value is retrieved by you,
whether from Python code or within templates. However, operations by
``django-tables``, like sorting, always work with the raw data.
diff --git a/tests/test_memory.py b/tests/test_memory.py
index fe5ef6b3..1f9252e6 100644
--- a/tests/test_memory.py
+++ b/tests/test_memory.py
@@ -14,8 +14,8 @@ def test_basic():
class StuffTable(tables.MemoryTable):
name = tables.Column()
answer = tables.Column(default=42)
- c = tables.Column(name="count", default=1)
- email = tables.Column(data="@")
+ count = tables.Column(model_rel="c", default=1)
+ email = tables.Column(model_rel="@")
stuff = StuffTable([
{'id': 1, 'name': 'Foo Bar', '@': 'foo@bar.org'},
])
@@ -26,8 +26,8 @@ class StuffTable(tables.MemoryTable):
# make sure BoundColumnn.name always gives us the right thing, whether
# the column explicitely defines a name or not.
- stuff.columns['count'].name == 'count'
- stuff.columns['answer'].name == 'answer'
+ assert stuff.columns['count'].name == 'count'
+ assert stuff.columns['answer'].name == 'answer'
for r in stuff.rows:
# unknown fields are removed/not-accessible
@@ -42,7 +42,7 @@ class StuffTable(tables.MemoryTable):
assert 'count' in r
assert r['count'] == 1
- # columns with data= option work fine
+ # columns with model_rel= option work fine
assert r['email'] == 'foo@bar.org'
# try to splice rows by index
@@ -76,7 +76,7 @@ class TestRender:
def test(self):
class TestTable(tables.MemoryTable):
- private_name = tables.Column(name='public_name')
+ public_name = tables.Column(model_rel='private_name')
def render_public_name(self, data):
# We are given the actual data dict and have direct access
# to additional values for which no field is defined.
@@ -150,9 +150,9 @@ def test_sort():
class BookTable(tables.MemoryTable):
id = tables.Column(direction='desc')
name = tables.Column()
- pages = tables.Column(name='num_pages') # test rewritten names
+ num_pages = tables.Column(model_rel='pages') # test rewritten names
language = tables.Column(default='en') # default affects sorting
- rating = tables.Column(data='*') # test data field option
+ rating = tables.Column(model_rel='*') # test data field option
books = BookTable([
{'id': 1, 'pages': 60, 'name': 'Z: The Book', '*': 5}, # language: en
diff --git a/tests/test_models.py b/tests/test_models.py
index 0b3f79a3..1d0520e9 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -44,16 +44,25 @@ class Meta:
call_command('syncdb', verbosity=1, interactive=False)
# create a couple of objects
- berlin=City(name="Berlin"); berlin.save()
- amsterdam=City(name="Amsterdam"); amsterdam.save()
- Country(name="Austria", tld="au", population=8, system="republic").save()
- Country(name="Germany", tld="de", population=81, capital=berlin).save()
- Country(name="France", tld="fr", population=64, system="republic").save()
- Country(name="Netherlands", tld="nl", population=16, system="monarchy", capital=amsterdam).save()
+ berlin = City.objects.create(name="Berlin", population=30)
+ amsterdam = City.objects.create(name="Amsterdam", population=6)
+ Country.objects.create(
+ name="Austria", tld="au", population=8, system="republic")
+ Country.objects.create(
+ name="Germany", tld="de", population=81, capital=berlin)
+ Country.objects.create(
+ name="France", tld="fr", population=64, system="republic")
+ Country.objects.create(
+ name="Netherlands",
+ tld="nl",
+ population=16,
+ system="monarchy",
+ capital=amsterdam)
class TestDeclaration:
- """Test declaration, declared columns and default model field columns.
+ """
+ Test declaration, declared columns and default model field columns.
"""
def test_autogen_basic(self):
@@ -85,13 +94,29 @@ class Meta:
columns = ['id', 'name']
exclude = ['capital']
- print CityTable.base_columns
assert len(CityTable.base_columns) == 4
assert 'id' in CityTable.base_columns
assert 'name' in CityTable.base_columns
assert 'projected' in CityTable.base_columns # declared in parent
assert not 'population' in CityTable.base_columns # not in Meta:columns
- assert 'capital' in CityTable.base_columns # in exclude, but only works on model fields (is that the right behaviour?)
+ # in exclude, but only works on model fields (is that the right behaviour?)
+ assert 'capital' in CityTable.base_columns
+
+ # Define one column so that all automatically-generated columns are
+ # excluded
+ class CountryTable(tables.ModelTable):
+ capital = tables.TextColumn(verbose_name='Name of capital')
+ projected = tables.Column(verbose_name="Projected Population")
+
+ class Meta:
+ model = Country
+ columns = ['capital']
+
+ num_columns = len(CountryTable.base_columns)
+ assert num_columns == 2, "Actual: %s" % num_columns
+ assert 'projected' in CountryTable.base_columns
+ assert 'capital' in CountryTable.base_columns
+ assert not 'tld' in CountryTable.base_columns
def test_columns_custom_order(self):
"""Using the columns meta option, you can also modify the ordering.
@@ -114,41 +139,44 @@ class Meta:
assert [c.column.verbose_name for c in CountryTable().columns] == ['Domain Extension']
+def _test_country_table(table):
+ for r in table.rows:
+ # "normal" fields exist
+ assert 'name' in r
+ # unknown fields are removed/not accessible
+ assert not 'does-not-exist' in r
+ # ...so are excluded fields
+ assert not 'id' in r
+ # [bug] access to data that might be available, but does not
+ # have a corresponding column is denied.
+ assert_raises(Exception, "r['id']")
+ # missing data is available with default values
+ assert 'null' in r
+ assert r['null'] == "foo" # note: different from prev. line!
+ # if everything else fails (no default), we get None back
+ assert r['null2'] is None
+
+ # all that still works when name overrides are used
+ assert 'tld' in r
+ assert 'domain' in r
+ assert len(r['domain']) == 2 # valid country tld
def test_basic():
- """Some tests here are copied from ``test_basic.py`` but need to be
- rerun with a ModelTable, as the implementation is different."""
+ """
+ Some tests here are copied from ``test_basic.py`` but need to be
+ rerun with a ModelTable, as the implementation is different.
+ """
class CountryTable(tables.ModelTable):
null = tables.Column(default="foo")
- tld = tables.Column(name="domain")
+ domain = tables.Column(model_rel="tld")
class Meta:
model = Country
exclude = ('id',)
- countries = CountryTable()
- def test_country_table(table):
- for r in table.rows:
- # "normal" fields exist
- assert 'name' in r
- # unknown fields are removed/not accessible
- assert not 'does-not-exist' in r
- # ...so are excluded fields
- assert not 'id' in r
- # [bug] access to data that might be available, but does not
- # have a corresponding column is denied.
- assert_raises(Exception, "r['id']")
- # missing data is available with default values
- assert 'null' in r
- assert r['null'] == "foo" # note: different from prev. line!
- # if everything else fails (no default), we get None back
- assert r['null2'] is None
-
- # all that still works when name overrides are used
- assert not 'tld' in r
- assert 'domain' in r
- assert len(r['domain']) == 2 # valid country tld
- test_country_table(countries)
+
+ countries = CountryTable()
+ _test_country_table(countries)
# repeat the avove tests with a table that is not associated with a
# model, and all columns being created manually.
@@ -159,10 +187,52 @@ class CountryTable(tables.ModelTable):
system = tables.Column()
null = tables.Column(default="foo")
null2 = tables.Column()
- tld = tables.Column(name="domain")
+ domain = tables.Column(model_rel="tld")
+ tld = tables.Column()
+
+
countries = CountryTable(Country)
- test_country_table(countries)
+ _test_country_table(countries)
+
+def test_with_filter():
+ class CountryTable(tables.ModelTable):
+ null = tables.Column(default="foo")
+ domain = tables.Column(model_rel="tld")
+ class Meta:
+ model = Country
+ exclude = ('id',)
+ countries = CountryTable(Country.objects.filter(name="France"))
+
+ assert len(countries.rows) == 1
+ row = countries.rows[0]
+ assert row['name'] == 'France'
+
+ _test_country_table(countries)
+
+def test_with_empty_list():
+ class CountryTable(tables.ModelTable):
+ null = tables.Column(default="foo")
+ domain = tables.Column(model_rel="tld")
+ class Meta:
+ model = Country
+ exclude = ('id',)
+ # Should be able to pass in an empty list and call order_by on it
+ countries = CountryTable([], order_by='domain')
+ assert len(countries.rows) == 0
+
+def test_with_no_results_query():
+ class CountryTable(tables.ModelTable):
+ null = tables.Column(default="foo")
+ domain = tables.Column(model_rel="tld")
+ class Meta:
+ model = Country
+ exclude = ('id',)
+
+ # Should be able to pass in an empty list and call order_by on it
+ countries = CountryTable(Country.objects.filter(name='does not exist'), order_by='domain')
+ assert len(countries.rows) == 0
+
def test_invalid_accessor():
"""Test that a column being backed by a non-existent model property
@@ -171,7 +241,7 @@ def test_invalid_accessor():
Regression-Test: There used to be a NameError here.
"""
class CountryTable(tables.ModelTable):
- name = tables.Column(data='something-i-made-up')
+ name = tables.Column(model_rel='something-i-made-up')
countries = CountryTable(Country)
assert_raises(ValueError, countries[0].__getitem__, 'name')
@@ -184,7 +254,7 @@ class CountryTable(tables.ModelTable):
class Meta:
model = Country
exclude = ('id',)
- countries = CountryTable()
+ countries = CountryTable(Country.objects.all())
assert id(list(countries.columns)[0]) == id(list(countries.columns)[0])
# TODO: row cache currently not used
@@ -199,18 +269,19 @@ class Meta:
def test_sort():
class CountryTable(tables.ModelTable):
- tld = tables.Column(name="domain")
+ domain = tables.Column(model_rel="tld")
population = tables.Column()
system = tables.Column(default="republic")
custom1 = tables.Column()
custom2 = tables.Column(sortable=True)
class Meta:
model = Country
- countries = CountryTable()
+ countries = CountryTable(Country.objects.all())
- def test_order(order, result, table=countries):
+ def test_order(order, expected, table=countries):
table.order_by = order
- assert [r['id'] for r in table.rows] == result
+ actual = [r['id'] for r in table.rows]
+ assert actual == expected, "actual= %s" % repr(actual)
# test various orderings
test_order(('population',), [1,4,3,2])
@@ -219,6 +290,8 @@ def test_order(order, result, table=countries):
# test sorting with a "rewritten" column name
countries.order_by = 'domain,tld' # "tld" would be invalid...
countries.order_by == ('domain',) # ...and is therefore removed
+ countries.order_by = ('-domain','tld') # "tld" would be invalid...
+ countries.order_by == ('-domain',) # ...and is therefore removed
test_order(('-domain',), [4,3,2,1])
# test multiple order instructions; note: one row is missing a "system"
# value, but has a default set; however, that has no effect on sorting.
@@ -232,7 +305,7 @@ class CityTable(tables.ModelTable):
name = tables.Column(direction='desc')
class Meta:
model = City
- cities = CityTable()
+ cities = CityTable(City.objects.all())
test_order('name', [1,2], table=cities) # Berlin to Amsterdam
test_order('-name', [2,1], table=cities) # Amsterdam to Berlin
@@ -242,7 +315,7 @@ class Meta:
# ...in case of ModelTables, this primarily means that only
# model-based colunns are currently sortable at all.
countries.order_by = ('custom1', 'custom2')
- assert countries.order_by == ()
+ assert countries.order_by == (), "Actual: %s" % repr(countries.order_by)
def test_default_sort():
class SortedCountryTable(tables.ModelTable):
@@ -250,16 +323,17 @@ class Meta:
model = Country
order_by = '-name'
+ countries = Country.objects.all()
# the order_by option is provided by TableOptions
- assert_equal('-name', SortedCountryTable()._meta.order_by)
+ assert_equal('-name', SortedCountryTable(countries)._meta.order_by)
# the default order can be inherited from the table
- assert_equal(('-name',), SortedCountryTable().order_by)
- assert_equal(4, SortedCountryTable().rows[0]['id'])
+ assert_equal(('-name',), SortedCountryTable(countries).order_by)
+ assert_equal(4, SortedCountryTable(countries).rows[0]['id'])
# and explicitly set (or reset) via __init__
- assert_equal(2, SortedCountryTable(order_by='system').rows[0]['id'])
- assert_equal(1, SortedCountryTable(order_by=None).rows[0]['id'])
+ assert_equal(2, SortedCountryTable(countries, order_by='system').rows[0]['id'])
+ assert_equal(1, SortedCountryTable(countries, order_by=None).rows[0]['id'])
def test_callable():
"""Some of the callable code is reimplemented for modeltables, so
@@ -287,11 +361,17 @@ def test_relationships():
class CountryTable(tables.ModelTable):
# add relationship spanning columns (using different approaches)
- capital_name = tables.Column(data='capital__name')
- capital__population = tables.Column(name="capital_population")
- invalid = tables.Column(data="capital__invalid")
+ capital_name = tables.Column(model_rel='capital__name')
+ capital_name_link = tables.Column(model_rel='capital__name')
+ cap_pop = tables.Column(model_rel="capital__population")
+ invalid = tables.Column(model_rel="capital__invalid")
class Meta:
model = Country
+
+ def render_capital_name_link(self, country):
+ return '%s' % (
+ country.capital.name, country.capital.name)
+
countries = CountryTable(Country.objects.select_related('capital'))
# ordering and field access works
@@ -299,13 +379,14 @@ class Meta:
assert [row['capital_name'] for row in countries.rows] == \
[None, None, 'Amsterdam', 'Berlin']
- countries.order_by = 'capital_population'
- assert [row['capital_population'] for row in countries.rows] == \
- [None, None, None, None]
+ countries.order_by = 'cap_pop'
+ actual = [row['cap_pop'] for row in countries.rows]
+ assert actual == \
+ [None, None, 6, 30], "Actual: %s" % repr(actual)
# ordering by a column with an invalid relationship fails silently
countries.order_by = 'invalid'
- assert countries.order_by == ()
+ assert countries.order_by == (), "Actual: %s" % repr(countries.order_by)
def test_pagination():
@@ -322,12 +403,12 @@ class CityTable(tables.ModelTable):
class Meta:
model = City
columns = ['name']
- cities = CityTable()
# add some sample data
City.objects.all().delete()
for i in range(1,101):
City.objects.create(name="City %d"%i)
+ cities = CityTable(City.objects.all())
# for query logging
settings.DEBUG = True
@@ -340,9 +421,9 @@ class Meta:
assert len(page.object_list) == 10
assert page.has_previous() == False
assert page.has_next() == True
- # Make sure the queryset is not loaded completely - there must be two
- # queries, one a count(). This check is far from foolproof...
- assert len(connection.queries)-start_querycount == 2
+ # Make sure the queryset is not loaded completely - there must be one
+ # query: count(). This check is far from foolproof...
+ assert len(connection.queries)-start_querycount == 1, len(connection.queries)-start_querycount
# using a queryset paginator is possible as well (although unnecessary)
paginator = QuerySetPaginator(cities.rows, 10)
@@ -358,7 +439,7 @@ class Meta:
assert cities.paginator.num_pages == 10
assert cities.page.has_previous() == False
assert cities.page.has_next() == True
- assert len(connection.queries)-start_querycount == 2
+ assert len(connection.queries)-start_querycount == 0
# reset
settings.DEBUG = False
diff --git a/tests/test_templates.py b/tests/test_templates.py
index 50f4dcb6..2c12ecaf 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -32,7 +32,8 @@ class CountryTable(tables.MemoryTable):
population = tables.NumberColumn(verbose_name="Population Size")
currency = tables.NumberColumn(visible=False, inaccessible=True)
tld = tables.TextColumn(visible=False, verbose_name="Domain")
- calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")
+ cc = tables.NumberColumn(
+ model_rel="calling_code", verbose_name="Phone Ext.")
countries = CountryTable(
[{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'cc': 49},
@@ -70,7 +71,9 @@ class CountryTable(tables.MemoryTable):
assert countries.columns['tld'].visible == False
def test_render():
- """For good measure, render some actual templates."""
+ """
+ For good measure, render some actual templates.
+ """
class CountryTable(tables.MemoryTable):
name = tables.TextColumn()
@@ -78,7 +81,8 @@ class CountryTable(tables.MemoryTable):
population = tables.NumberColumn(verbose_name="Population Size")
currency = tables.NumberColumn(visible=False, inaccessible=True)
tld = tables.TextColumn(visible=False, verbose_name="Domain")
- calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")
+ cc = tables.NumberColumn(
+ model_rel="calling_code", verbose_name="Phone Ext.")
countries = CountryTable(
[{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'calling_code': 49},
@@ -98,6 +102,37 @@ class CountryTable(tables.MemoryTable):
render(Context({'countries': countries})) == \
"Germany France Netherlands Austria"
+def test_custom_render():
+ class CountryTable(tables.MemoryTable):
+ name = tables.TextColumn()
+ capital = tables.TextColumn()
+ population = tables.NumberColumn(verbose_name="Population Size")
+ currency = tables.NumberColumn(visible=False, inaccessible=True)
+ tld = tables.TextColumn(visible=False, verbose_name="Domain")
+ cc = tables.NumberColumn(
+ model_rel="calling_code", verbose_name="Phone Ext.")
+
+ def render_cc(self, src_row):
+ return "SUCCESS"
+
+ def render_calling_code(self, src_row):
+ # We should never get here
+ assert False
+
+ countries = CountryTable(
+ [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'calling_code': 49},
+ {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'calling_code': 33},
+ {'name': 'Netherlands', 'capital': 'Amsterdam', 'calling_code': '31'},
+ {'name': 'Austria', 'calling_code': 43, 'currency': 'Euro (€)', 'population': 8}])
+
+ assert Template("{% for column in countries.columns %}{{ column }}/{{ column.name }} {% endfor %}").\
+ render(Context({'countries': countries})) == \
+ "Name/name Capital/capital Population Size/population Phone Ext./cc "
+
+ assert Template("{% for row in countries %}{% for value in row %}{{ value }} {% endfor %}{% endfor %}").\
+ render(Context({'countries': countries})) == \
+ "Germany Berlin 83 SUCCESS France None 64 SUCCESS Netherlands Amsterdam None SUCCESS Austria None 8 SUCCESS "
+
def test_templatetags():
add_to_builtins('django_tables.app.templatetags.tables')