Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions README

This file was deleted.

67 changes: 67 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -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.
107 changes: 58 additions & 49 deletions django_tables/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -360,18 +355,21 @@ 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):
result = result(self)
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:
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
"""
Expand All @@ -518,34 +526,35 @@ 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:
name = ident
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.

Expand Down
29 changes: 7 additions & 22 deletions django_tables/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,18 @@ 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
that whether this effects ordering might depend on the table type (model
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
Expand All @@ -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
Expand Down
Loading