*.pyc
-
/MANIFEST
-/dist
-/docs/_build/*
-/build
-
-/BRANCH_TODO
-
-# Project files
-/.project
-/.pydevproject
-/*.wpr
+dist/
+docs/_build/
+django_tables.egg-info/
+++ /dev/null
-Copyright (c) 2008, Michael Elsdörfer <http://elsdoerfer.name>
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
- 1. Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
-
- 2. Redistributions in binary form must reproduce the above
- copyright notice, this list of conditions and the following
- disclaimer in the documentation and/or other materials
- provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
+++ /dev/null
-include README
-include LICENSE
-include MANIFEST.in
-recursive-include tests *.py
+++ /dev/null
-django-tables - a Django QuerySet renderer.
-
-Documentation:
- http://elsdoerfer.name/docs/django-tables/
+++ /dev/null
-Document how the user can access the raw row data; and possible make this
-easier by falling back to the raw data directly if a column is accessed
-which doesn't exist.
-
-There's no particular reason why this should be Django-specific. Now with
-the base table better abstracted, we should be able to easily create a
-SQLAlchemyTable or a StormTable.
-
-If the table were passed a ``request`` object, it could generate columns
-proper sort links without requiring the set_url_param tag. However, that
-might introduce a Django dependency. Possibly rather than the request we
-could expect a dict of query string values.
-
-It would be cool if for non-model tables, a custom compare function could
-be provided to modify the sort. This would require a minor refactor in
-which we have multiple different table types subclass a base table, and
-the subclass allowing it's columns to support additional kwargs.
-
-"data", is used to format for display, affect sorting; this stuff needs
-some serious redesign.
-
-as_html methods are all empty right now
-
-table.column[].values is a stub
-
-Filters + grouping
-
-Choices support for columns (internal column value will be looked up for
-output
-
-For columns that span model relationships, automatically generate
-select_related(); this is important, since right now such an e.g.
-order_by would cause rows to be dropped (inner join).
-
-Initialize auto-generated columns with the relevant properties of the model
-fields (verbose_name, editable=visible?, ...)
-
-Remove support for callable fields? this has become obsolete since we
-Column.data property; also, it's easy to make the call manually, or let
-the template engine handle it.
-
-Tests could use some refactoring, they are currently all over the place
-
-What happens if duplicate column names are used? we currently don't check
-for that at all.
-
-
-Filters
-~~~~~~~
-
-Filtering is already easy (just use the normal queryset methods), but
-filter support in django-tables would want to hide the Django ORM syntax
-from the user.
-
- * For example, say a ``models.DateTimeField`` should be filtered
- by year: the user would only see ``date=2008`` rather than maybe
- ``published_at__year=2008``.
-
- * Say you want to filter out ``UserProfile`` rows that do not have
- an avatar image set. The user would only see ```no_avatar``, which
- in Django ORM syntax might map to
- ``Q(avatar__isnull=True) | Q(avatar='')``.
-
-Filters would probably always belong to a column, and be defined along
-side one.
-
- class BookTable(tables.ModelTable):
- date = tables.Column(filter='published_at__year')
-
-If a filter is needed that does not belong to a single colunn, a column
-would have to be defined for just that filter. A ``tables.Filter`` object
-could be provided that would basically be a column, but with default
-properties set so that the column functionality is disabled as far as
-possible (i.e. ``visible=False`` etc):
-
- class BookTable(tables.ModelTable):
- date = tables.Column(filter='published_at__year')
- has_cover = tables.Filter('cover__isnull', value=True)
-
-Or, if Filter() gets a lot of additional functionality like ``value``,
-we could generally make it available to all filters like so:
-
- class BookTable(tables.ModelTable):
- date = tables.Column(filter=tables.Filter('published_at__year', default=2008))
- has_cover = tables.Filter('cover__isnull', value=True)
-
-More complex filters should be supported to (e.g. combine multiple Q
-objects, support ``exclude`` as well as ``filter``). Allowing the user
-to somehow specify a callable probably is the easiest way to enable this.
-
-The filter querystring syntax, as exposed to the user, could look like this:
-
- /view/?filter=name:value
- /view/?filter=name
-
-It would also be cool if filters could be combined. However, in that case
-it would also make sense to make it possible to choose individual filters
-which cannot be combined with any others, or maybe even allow the user
-to specify complex dependencies. That may be pushing it though, and anyway
-won't make it into the first version.
-
- /view/?filter=name:value,foo:bar
-
-We need to think about how we make the functionality available to
-templates.
-
-Another feature that would be important is the ability to limit the valid
-values for a filter, e.g. in the date example only years from 2000 to 2008.
-
-Use django-filters:
- - would support html output
- - would not work at all with our planned QueryTable
- - conflicts somewhat in that it also allows ordering
-
-To autoamtically allow filtering a column with filter=True, we would need to
-have subclasses for each model class, even if it just redirects to use the
-correct filter class;
-
-If not using django-filter, we wouldn't have different filter types; filters
-would just hold the data, and each column would know how to apply it.
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Komodo Project File - DO NOT EDIT -->
+<project id="b062a53e-4e42-074d-bae5-c279107e8f41" kpf_version="5" name="django-tables.komodoproject">
+<preference-set idref="b062a53e-4e42-074d-bae5-c279107e8f41">
+ <string relative="path" id="import_dirname"></string>
+ <string id="import_exclude_matches">*.*~;*.bak;*.tmp;CVS;.#*;*.pyo;*.pyc;.svn;*%*;tmp*.html;.DS_Store;.komodotools;*.komodoproject;*.egg-info;.git</string>
+ <string id="import_include_matches"></string>
+ <boolean id="import_live">1</boolean>
+</preference-set>
+</project>
-__version__ = (0, 3, 'dev')
+# -*- coding: utf-8 -*-
+__version__ = (0, 2, 0, 'dev')
-from memory import *
-from models import *
-from columns import *
-from options import *
+def get_version():
+ version = '%s.%s' % (__version__[0], __version__[1])
+ if __version__[2]:
+ version = '%s.%s' % (version, __version__[2])
+ if __version__[3] != '':
+ version = '%s %s' % (version, __version__[3])
+ return version
+
+# We want to make get_version() available to setup.py even if Django is not
+# available or we are not inside a Django project (so we do distutils stuff).
+try:
+ # this fails if project settings module isn't configured
+ from django.contrib import admin
+except ImportError:
+ import warnings
+ warnings.warn('django-tables requires Django to be configured (settings) '
+ 'prior to use, however this has not been done. Version information '
+ 'will still be available.')
+else:
+ from tables import *
+ from columns import *
+++ /dev/null
-# Empty models.py file required for Django
-# INSTALLED_APPS loading.
+++ /dev/null
-# coding: utf8
-"""
-Allows setting/changing/removing of chosen url query string parameters,
-while maintaining any existing others.
-
-Expects the current request to be available in the context as ``request``.
-
-Examples:
-
- {% set_url_param page=next_page %}
- {% set_url_param page="" %}
- {% set_url_param filter="books" page=1 %}
-"""
-
-import urllib
-import tokenize
-import StringIO
-from django import template
-from django.utils.safestring import mark_safe
-
-register = template.Library()
-
-class SetUrlParamNode(template.Node):
- def __init__(self, changes):
- self.changes = changes
-
- def render(self, context):
- request = context.get('request', None)
- if not request: return ""
-
- # Note that we want params to **not** be a ``QueryDict`` (thus we
- # don't use it's ``copy()`` method), as it would force all values
- # to be unicode, and ``urllib.urlencode`` can't handle that.
- params = dict(request.GET)
- for key, newvalue in self.changes.items():
- newvalue = newvalue.resolve(context)
- if newvalue=='' or newvalue is None: params.pop(key, False)
- else: params[key] = unicode(newvalue)
- # ``urlencode`` chokes on unicode input, so convert everything to
- # utf8. Note that if some query arguments passed to the site have
- # their non-ascii characters screwed up when passed though this,
- # it's most likely not our fault. Django (the ``QueryDict`` class
- # to be exact) uses your projects DEFAULT_CHARSET to decode incoming
- # query strings, whereas your browser might encode the url
- # differently. For example, typing "ä" in my German Firefox's (v2)
- # address bar results in "%E4" being passed to the server (in
- # iso-8859-1), but Django might expect utf-8, where ä would be
- # "%C3%A4"
- def mkstr(s):
- if isinstance(s, list): return map(mkstr, s)
- else: return (isinstance(s, unicode) and [s.encode('utf-8')] or [s])[0]
- params = dict([(mkstr(k), mkstr(v)) for k, v in params.items()])
- # done, return (string is already safe)
- return '?'+urllib.urlencode(params, doseq=True)
-
-def do_seturlparam(parser, token):
- bits = token.contents.split()
- qschanges = {}
- for i in bits[1:]:
- try:
- a, b = i.split('=', 1); a = a.strip(); b = b.strip()
- keys = list(tokenize.generate_tokens(StringIO.StringIO(a).readline))
- if keys[0][0] == tokenize.NAME:
- if b == '""': b = template.Variable('""') # workaround bug #5270
- else: b = parser.compile_filter(b)
- qschanges[str(a)] = b
- else: raise ValueError
- except ValueError:
- raise template.TemplateSyntaxError, "Argument syntax wrong: should be key=value"
- return SetUrlParamNode(qschanges)
-
-register.tag('set_url_param', do_seturlparam)
+++ /dev/null
-import copy
-from django.http import Http404
-from django.core import paginator
-from django.utils.datastructures import SortedDict
-from django.utils.encoding import force_unicode, StrAndUnicode
-from django.utils.text import capfirst
-from columns import Column
-from options import options
-
-
-__all__ = ('BaseTable', 'options')
-
-
-class TableOptions(object):
- def __init__(self, options=None):
- super(TableOptions, self).__init__()
- self.sortable = getattr(options, 'sortable', None)
- self.order_by = getattr(options, 'order_by', None)
-
-
-class DeclarativeColumnsMetaclass(type):
- """
- Metaclass that converts Column attributes to a dictionary called
- 'base_columns', taking into account parent class 'base_columns'
- as well.
- """
- def __new__(cls, name, bases, attrs, parent_cols_from=None):
- """
- The ``parent_cols_from`` argument determins from which attribute
- we read the columns of a base class that this table might be
- subclassing. This is useful for ``ModelTable`` (and possibly other
- derivatives) which might want to differ between the declared columns
- and others.
-
- Note that if the attribute specified in ``parent_cols_from`` is not
- found, we fall back to the default (``base_columns``), instead of
- skipping over that base. This makes a table like the following work:
-
- class MyNewTable(tables.ModelTable, MyNonModelTable):
- pass
-
- ``MyNewTable`` will be built by the ModelTable metaclass, which will
- call this base with a modified ``parent_cols_from`` argument
- specific to ModelTables. Since ``MyNonModelTable`` is not a
- ModelTable, and thus does not provide that attribute, the columns
- from that base class would otherwise be ignored.
- """
-
- # extract declared columns
- columns = [(column_name, attrs.pop(column_name))
- for column_name, obj in attrs.items()
- if isinstance(obj, Column)]
- columns.sort(lambda x, y: cmp(x[1].creation_counter,
- y[1].creation_counter))
-
- # If this class is subclassing other tables, add their fields as
- # well. Note that we loop over the bases in *reverse* - this is
- # necessary to preserve the correct order of columns.
- for base in bases[::-1]:
- col_attr = (parent_cols_from and hasattr(base, parent_cols_from)) \
- and parent_cols_from\
- or 'base_columns'
- if hasattr(base, col_attr):
- columns = getattr(base, col_attr).items() + columns
- # Note that we are reusing an existing ``base_columns`` attribute.
- # This is because in certain inheritance cases (mixing normal and
- # ModelTables) this metaclass might be executed twice, and we need
- # to avoid overriding previous data (because we pop() from attrs,
- # the second time around columns might not be registered again).
- # An example would be:
- # class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass
- if not 'base_columns' in attrs:
- attrs['base_columns'] = SortedDict()
- attrs['base_columns'].update(SortedDict(columns))
-
- attrs['_meta'] = TableOptions(attrs.get('Meta', None))
- return type.__new__(cls, name, bases, attrs)
-
-
-def rmprefix(s):
- """Normalize a column name by removing a potential sort prefix"""
- return (s[:1]=='-' and [s[1:]] or [s])[0]
-
-def toggleprefix(s):
- """Remove - prefix is existing, or add if missing."""
- return ((s[:1] == '-') and [s[1:]] or ["-"+s])[0]
-
-class OrderByTuple(tuple, StrAndUnicode):
- """Stores 'order by' instructions; Used to render output in a format
- we understand as input (see __unicode__) - especially useful in
- templates.
-
- Also supports some functionality to interact with and modify
- the order.
- """
- def __unicode__(self):
- """Output in our input format."""
- return ",".join(self)
-
- def __contains__(self, name):
- """Determine whether a column is part of this order."""
- for o in self:
- if rmprefix(o) == name:
- return True
- return False
-
- def is_reversed(self, name):
- """Returns a bool indicating whether the column is ordered
- reversed, None if it is missing."""
- for o in self:
- if o == '-'+name:
- return True
- return False
- def is_straight(self, name):
- """The opposite of is_reversed."""
- for o in self:
- if o == name:
- return True
- return False
-
- def polarize(self, reverse, names=()):
- """Return a new tuple with the columns from ``names`` set to
- "reversed" (e.g. prefixed with a '-'). Note that the name is
- ambiguous - do not confuse this with ``toggle()``.
-
- If names is not specified, all columns are reversed. If a
- column name is given that is currently not part of the order,
- it is added.
- """
- prefix = reverse and '-' or ''
- return OrderByTuple(
- [
- (
- # add either untouched, or reversed
- (names and rmprefix(o) not in names)
- and [o]
- or [prefix+rmprefix(o)]
- )[0]
- for o in self]
- +
- [prefix+name for name in names if not name in self]
- )
-
- def toggle(self, names=()):
- """Return a new tuple with the columns from ``names`` toggled
- with respect to their "reversed" state. E.g. a '-' prefix will
- be removed is existing, or added if lacking. Do not confuse
- with ``reverse()``.
-
- If names is not specified, all columns are toggled. If a
- column name is given that is currently not part of the order,
- it is added in non-reverse form."""
- return OrderByTuple(
- [
- (
- # add either untouched, or toggled
- (names and rmprefix(o) not in names)
- and [o]
- or ((o[:1] == '-') and [o[1:]] or ["-"+o])
- )[0]
- for o in self]
- +
- [name for name in names if not name in self]
- )
-
-
-class Columns(object):
- """Container for spawning BoundColumns.
-
- This is bound to a table and provides it's ``columns`` property. It
- provides access to those columns in different ways (iterator,
- item-based, filtered and unfiltered etc)., stuff that would not be
- possible with a simple iterator in the table class.
-
- Note that when you define your column using a name override, e.g.
- ``author_name = tables.Column(name="author")``, then the column will
- be exposed by this container as "author", not "author_name".
- """
- def __init__(self, table):
- self.table = table
- self._columns = SortedDict()
-
- def _reset(self):
- """Used by parent table class."""
- self._columns = SortedDict()
-
- def _spawn_columns(self):
- # (re)build the "_columns" cache of BoundColumn objects (note that
- # ``base_columns`` might have changed since last time); creating
- # 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]
- else:
- new_columns[exposed_name] = BoundColumn(self.table, column, decl_name)
- self._columns = new_columns
-
- def all(self):
- """Iterate through all columns, regardless of visiblity (as
- opposed to ``__iter__``.
-
- This is used internally a lot.
- """
- self._spawn_columns()
- for column in self._columns.values():
- yield column
-
- def items(self):
- self._spawn_columns()
- for r in self._columns.items():
- yield r
-
- def names(self):
- self._spawn_columns()
- for r in self._columns.keys():
- yield r
-
- def index(self, name):
- self._spawn_columns()
- return self._columns.keyOrder.index(name)
-
- def sortable(self):
- """Iterate through all sortable columns.
-
- This is primarily useful in templates, where iterating over the full
- set and checking {% if column.sortable %} can be problematic in
- conjunction with e.g. {{ forloop.last }} (the last column might not
- be the actual last that is rendered).
- """
- for column in self.all():
- if column.sortable:
- yield column
-
- def __iter__(self):
- """Iterate through all *visible* bound columns.
-
- This is primarily geared towards table rendering.
- """
- for column in self.all():
- if column.visible:
- yield column
-
- def __contains__(self, item):
- """Check by both column object and column name."""
- self._spawn_columns()
- if isinstance(item, basestring):
- return item in self.names()
- else:
- return item in self.all()
-
- def __len__(self):
- self._spawn_columns()
- return len([1 for c in self._columns.values() if c.visible])
-
- def __getitem__(self, name):
- """Return a column by name."""
- self._spawn_columns()
- return self._columns[name]
-
-
-class BoundColumn(StrAndUnicode):
- """'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):
- self.table = table
- self.column = column
- self.declared_name = name
- # expose some attributes of the column more directly
- self.visible = column.visible
-
- @property
- def 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
-
- def _get_sortable(self):
- if self.column.sortable is not None:
- return self.column.sortable
- elif self.table._meta.sortable is not None:
- return self.table._meta.sortable
- else:
- return True # the default value
- sortable = property(_get_sortable)
-
- name = property(lambda s: s.column.name or s.declared_name)
- name_reversed = property(lambda s: "-"+s.name)
- def _get_name_toggled(self):
- o = self.table.order_by
- if (not self.name in o) or o.is_reversed(self.name): return self.name
- else: return self.name_reversed
- name_toggled = property(_get_name_toggled)
-
- is_ordered = property(lambda s: s.name in s.table.order_by)
- is_ordered_reverse = property(lambda s: s.table.order_by.is_reversed(s.name))
- is_ordered_straight = property(lambda s: s.table.order_by.is_straight(s.name))
- order_by = property(lambda s: s.table.order_by.polarize(False, [s.name]))
- order_by_reversed = property(lambda s: s.table.order_by.polarize(True, [s.name]))
- order_by_toggled = property(lambda s: s.table.order_by.toggle([s.name]))
-
- def get_default(self, row):
- """Since a column's ``default`` property may be a callable, we need
- this function to resolve it when needed.
-
- Make sure ``row`` is a ``BoundRow`` object, since that is what
- we promise the callable will get.
- """
- if callable(self.column.default):
- 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))
-
- def as_html(self):
- pass
-
-
-class BoundRow(object):
- """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.
- """
- def __init__(self, table, data):
- self.table = table
- self.data = data
-
- def __iter__(self):
- for value in self.values:
- 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."""
-
- column = self.table.columns[name]
-
- render_func = getattr(self.table, 'render_%s' % name, False)
- if render_func:
- return render_func(self.data)
- else:
- return self._default_render(column)
-
- def _default_render(self, column):
- """Returns a cell's content. This is used unless the user
- provides a custom ``render_FOO`` method.
- """
- result = self.data[column.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."""
- if isinstance(item, basestring):
- return item in self.table._columns
- else:
- return item in self
-
- def _get_values(self):
- for column in self.table.columns:
- yield self[column.name]
- values = property(_get_values)
-
- def as_html(self):
- pass
-
-
-class Rows(object):
- """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
- iterator in the table class.
- """
-
- row_class = BoundRow
-
- def __init__(self, table):
- self.table = table
-
- def _reset(self):
- pass # we currently don't use a cache
-
- def all(self):
- """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)."""
- if not hasattr(self.table, 'page'):
- return None
- return iter(self.table.page.object_list)
-
- def __iter__(self):
- return iter(self.all())
-
- def __len__(self):
- return len(self.table.data)
-
- def __getitem__(self, key):
- if isinstance(key, slice):
- result = list()
- for row in self.table.data[key]:
- result.append(self.row_class(self.table, row))
- return result
- elif isinstance(key, int):
- return self.row_class(self.table, self.table.data[key])
- else:
- raise TypeError('Key must be a slice or integer.')
-
-
-class BaseTable(object):
- """A collection of columns, plus their associated data rows.
- """
-
- __metaclass__ = DeclarativeColumnsMetaclass
-
- rows_class = Rows
-
- # this value is not the same as None. it means 'use the default sort
- # order', which may (or may not) be inherited from the table options.
- # None means 'do not sort the data', ignoring the default.
- DefaultOrder = type('DefaultSortType', (), {})()
-
- def __init__(self, data, order_by=DefaultOrder):
- """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.
-
- Note that unlike a ``Form``, tables are always bound to data. Also
- unlike a form, the ``columns`` attribute is read-only and returns
- ``BoundColum`` wrappers, similar to the ``BoundField``'s you get
- when iterating over a form. This is because the table iterator
- already yields rows, and we need an attribute via which to expose
- the (visible) set of (bound) columns - ``Table.columns`` is simply
- the perfect fit for this. Instead, ``base_colums`` is copied to
- table instances, so modifying that will not touch the class-wide
- column list.
- """
- self._data = data
- self._snapshot = None # will store output dataset (ordered...)
- self._rows = self.rows_class(self)
- self._columns = Columns(self)
-
- # None is a valid order, so we must use DefaultOrder as a flag
- # to fall back to the table sort order. set the attr via the
- # property, to wrap it in an OrderByTuple before being stored
- if order_by != BaseTable.DefaultOrder:
- self.order_by = order_by
-
- else:
- self.order_by = self._meta.order_by
-
- # Make a copy so that modifying this will not touch the class
- # definition. Note that this is different from forms, where the
- # copy is made available in a ``fields`` attribute. See the
- # ``Table`` class docstring for more information.
- self.base_columns = copy.deepcopy(type(self).base_columns)
-
- def _reset_snapshot(self, reason):
- """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
- given change may not affect their snaptshot.
- """
- self._snapshot = None
-
- def _build_snapshot(self):
- """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
- the linked data source.
-
- Subclasses should override this.
- """
- return self._data
-
- def _get_data(self):
- if self._snapshot is None:
- self._snapshot = self._build_snapshot()
- return self._snapshot
- 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
- according to each column's ``direction`` option, e.g. it translates
- between the ascending/descending and the straight/reverse terminology.
- """
- result = []
- for inst in order_by:
- if self.columns[rmprefix(inst)].column.direction == Column.DESC:
- inst = toggleprefix(inst)
- result.append(inst)
- return result
-
- def _cols_to_fields(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``.
-
- Supports prefixed column names as used e.g. in order_by ("-field").
- """
- result = []
- for ident in names:
- # handle order prefix
- if ident[:1] == '-':
- name = ident[1:]
- prefix = '-'
- else:
- name = ident
- prefix = ''
- # find the field name
- column = self.columns[name]
- result.append(prefix + column.accessor)
- return result
-
- def _validate_column_name(self, name, purpose):
- """Return True/False, depending on whether the column ``name`` is
- valid for ``purpose``. Used to validate things like ``order_by``
- instructions.
-
- Can be overridden by subclasses to impose further restrictions.
- """
- if purpose == 'order_by':
- return name in self.columns and\
- self.columns[name].sortable
- else:
- return True
-
- def _set_order_by(self, value):
- self._reset_snapshot('order_by')
- # accept both string and tuple instructions
- order_by = (isinstance(value, basestring) \
- and [value.split(',')] \
- or [value])[0]
- if order_by:
- # validate, remove all invalid order instructions
- validated_order_by = []
- for o in order_by:
- if self._validate_column_name(rmprefix(o), "order_by"):
- validated_order_by.append(o)
- elif not options.IGNORE_INVALID_OPTIONS:
- raise ValueError('Column name %s is invalid.' % o)
- self._order_by = OrderByTuple(validated_order_by)
- else:
- self._order_by = OrderByTuple()
-
- order_by = property(lambda s: s._order_by, _set_order_by)
-
- def __unicode__(self):
- return self.as_html()
-
- def __iter__(self):
- for row in self.rows:
- yield row
-
- def __getitem__(self, key):
- return self.rows[key]
-
- # just to make those readonly
- columns = property(lambda s: s._columns)
- rows = property(lambda s: s._rows)
-
- def as_html(self):
- pass
-
- def update(self):
- """Update the table based on it's current options.
-
- Normally, you won't have to call this method, since the table
- updates itself (it's caches) automatically whenever you change
- any of the properties. However, in some rare cases those
- changes might not be picked up, for example if you manually
- change ``base_columns`` or any of the columns in it.
- """
- self._build_snapshot()
-
- def paginate(self, klass, *args, **kwargs):
- page = kwargs.pop('page', 1)
- self.paginator = klass(self.rows, *args, **kwargs)
- try:
- self.page = self.paginator.page(page)
- except paginator.InvalidPage, e:
- raise Http404(str(e))
-__all__ = (
- 'Column', 'TextColumn', 'NumberColumn',
-)
+# -*- coding: utf-8 -*-
+from django.utils.encoding import force_unicode, StrAndUnicode
+from django.utils.datastructures import SortedDict
+from django.utils.text import capfirst
+
class Column(object):
"""Represents a single column of a table.
``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.
-
- ``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
descending using ``direction``. Note that this option changes the actual
direction only indirectly. Normal und reverse order, the terms
django-tables exposes, now simply mean different things.
- """
-
- ASC = 1
- DESC = 2
+ Data can be formatted by using ``formatter``, which accepts a callable as
+ an argument (e.g. lambda x: x.upper())
+ """
# 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,
- visible=True, inaccessible=False, sortable=None,
- direction=ASC):
+ def __init__(self, verbose_name=None, accessor=None, default=None,
+ visible=True, sortable=None, formatter=None):
+ if not (accessor is None or isinstance(accessor, basestring) or
+ callable(accessor)):
+ raise TypeError('accessor must be a string or callable, not %s' %
+ accessor.__class__.__name__)
+ if callable(accessor) and default is not None:
+ raise TypeError('accessor must be string when default is used, not'
+ ' callable')
+ self.accessor = accessor
+ self._default = default
+ self.formatter = formatter
+ self.sortable = sortable
self.verbose_name = verbose_name
- self.name = name
- 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
- self.direction = direction
self.creation_counter = Column.creation_counter
Column.creation_counter += 1
- def _set_direction(self, value):
- if isinstance(value, basestring):
- if value in ('asc', 'desc'):
- self._direction = (value == 'asc') and Column.ASC or Column.DESC
- else:
- raise ValueError('Invalid direction value: %s' % value)
+ @property
+ def default(self):
+ """Since ``Column.default`` property may be a callable, this function
+ handles access.
+ """
+ return self._default() if callable(self._default) else self._default
+
+ def render(self, table, bound_column, bound_row):
+ """Returns a cell's content.
+ This method can be overridden by ``render_FOO`` methods on the table or
+ by subclassing ``Column``.
+ """
+ return table.data.data_for_cell(bound_column=bound_column,
+ bound_row=bound_row)
+
+
+class CheckBoxColumn(Column):
+ """A subclass of Column that renders its column data as a checkbox
+
+ ``name`` is the html name of the checkbox.
+ """
+ def __init__(self, attrs=None, *args, **kwargs):
+ super(CheckBoxColumn, self).__init__(*args, **kwargs)
+ self.attrs = attrs or {}
+
+ def render(self, bound_column, bound_row):
+ from django.template import Template, Context
+ attrs = {'name': bound_column.name}
+ 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):
+ """'Runtime' version of ``Column`` that is bound to a table instance,
+ and thus knows about the table's data. The difference between BoundColumn
+ and Column, is a BoundColumn is aware of actual values (e.g. its name)
+ where-as Column is not.
+
+ For convenience, all Column properties are available from this class.
+ """
+ def __init__(self, table, column, name):
+ """*table* - the table in which this column exists
+ *column* - the column class
+ *name* – the variable name used when the column was defined in the
+ table class
+ """
+ self.table = table
+ self.column = column
+ self.name = name
+
+ def __unicode__(self):
+ s = self.column.verbose_name or self.name.replace('_', ' ')
+ return capfirst(force_unicode(s))
+
+ @property
+ def accessor(self):
+ return self.column.accessor or self.name
+
+ @property
+ def default(self):
+ return self.column.default
+
+ @property
+ def formatter(self):
+ return self.column.formatter
+
+ @property
+ def sortable(self):
+ if self.column.sortable is not None:
+ return self.column.sortable
+ elif self.table._meta.sortable is not None:
+ return self.table._meta.sortable
else:
- self._direction = value
+ return True # the default value
- direction = property(lambda s: s._direction, _set_direction)
+ @property
+ def verbose_name(self):
+ return self.column.verbose_name
+ @property
+ def visible(self):
+ return self.column.visible
-class TextColumn(Column):
- pass
-class NumberColumn(Column):
- pass
+class Columns(object):
+ """Container for spawning BoundColumns.
+
+ This is bound to a table and provides its ``columns`` property. It
+ provides access to those columns in different ways (iterator,
+ item-based, filtered and unfiltered etc), stuff that would not be
+ possible with a simple iterator in the table class.
+ """
+ def __init__(self, table):
+ self.table = table
+ # ``self._columns`` attribute stores the bound columns (columns that
+ # have a real name, )
+ self._columns = SortedDict()
+
+ def _spawn_columns(self):
+ # (re)build the "_columns" cache of BoundColumn objects (note that
+ # ``base_columns`` might have changed since last time); creating
+ # BoundColumn instances can be costly, so we reuse existing ones.
+ new_columns = SortedDict()
+ for name, column in self.table.base_columns.items():
+ if name in self._columns:
+ new_columns[name] = self._columns[name]
+ else:
+ new_columns[name] = BoundColumn(self.table, column, name)
+ self._columns = new_columns
+
+ def all(self):
+ """Iterate through all columns, regardless of visiblity (as
+ opposed to ``__iter__``.
+
+ This is used internally a lot.
+ """
+ self._spawn_columns()
+ for column in self._columns.values():
+ yield column
+
+ def items(self):
+ self._spawn_columns()
+ for r in self._columns.items():
+ yield r
+
+ def names(self):
+ self._spawn_columns()
+ for r in self._columns.keys():
+ yield r
+
+ def index(self, name):
+ self._spawn_columns()
+ return self._columns.keyOrder.index(name)
+
+ def sortable(self):
+ """Iterate through all sortable columns.
+
+ This is primarily useful in templates, where iterating over the full
+ set and checking {% if column.sortable %} can be problematic in
+ conjunction with e.g. {{ forloop.last }} (the last column might not
+ be the actual last that is rendered).
+ """
+ for column in self.all():
+ if column.sortable:
+ yield column
+
+ def __iter__(self):
+ """Iterate through all *visible* bound columns.
+
+ This is primarily geared towards table rendering.
+ """
+ for column in self.all():
+ if column.visible:
+ yield column
+
+ def __contains__(self, item):
+ """Check by both column object and column name."""
+ self._spawn_columns()
+ if isinstance(item, basestring):
+ return item in self.names()
+ else:
+ return item in self.all()
+
+ def __len__(self):
+ self._spawn_columns()
+ return len([1 for c in self._columns.values() if c.visible])
+
+ def __getitem__(self, index):
+ """Return a column by name or index."""
+ self._spawn_columns()
+ if isinstance(index, int):
+ return self._columns.value_for_index(index)
+ elif isinstance(index, basestring):
+ return self._columns[index]
+ else:
+ raise TypeError('row indices must be integers or str, not %s' %
+ index.__class__.__name__)
+++ /dev/null
-import copy
-from base import BaseTable, BoundRow
-
-
-__all__ = ('MemoryTable', 'Table',)
-
-
-def sort_table(data, order_by):
- """Sort a list of dicts according to the fieldnames in the
- ``order_by`` iterable. Prefix with hypen for reverse.
-
- Dict values can be callables.
- """
- def _cmp(x, y):
- for name, reverse in instructions:
- lhs, rhs = x.get(name), y.get(name)
- res = cmp((callable(lhs) and [lhs(x)] or [lhs])[0],
- (callable(rhs) and [rhs(y)] or [rhs])[0])
- if res != 0:
- return reverse and -res or res
- return 0
- instructions = []
- for o in order_by:
- if o.startswith('-'):
- instructions.append((o[1:], True,))
- else:
- instructions.append((o, False,))
- data.sort(cmp=_cmp)
-
-
-class MemoryTable(BaseTable):
- """Table that is based on an in-memory dataset (a list of dict-like
- objects).
- """
-
- def _build_snapshot(self):
- """Rebuilds the table whenever it's options change.
-
- Whenver the table options change, e.g. say a new sort order,
- this method will be asked to regenerate the actual table from
- the linked data source.
-
- In the case of this base table implementation, a copy of the
- source data is created, and then modified appropriately.
-
- # TODO: currently this is called whenever data changes; it is
- # 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)!
- # 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 self.order_by:
- actual_order_by = self._resolve_sort_directions(self.order_by)
- sort_table(snapshot, self._cols_to_fields(actual_order_by))
- return snapshot
-
-
-class Table(MemoryTable):
- def __new__(cls, *a, **kw):
- from warnings import warn
- warn('"Table" has been renamed to "MemoryTable". Please use the '+
- 'new name.', DeprecationWarning)
- return MemoryTable.__new__(cls)
-from django.core.exceptions import FieldError
-from django.utils.datastructures import SortedDict
-from base import BaseTable, DeclarativeColumnsMetaclass, \
- Column, BoundRow, Rows, TableOptions, rmprefix, toggleprefix
-
-
-__all__ = ('ModelTable',)
-
-
-class ModelTableOptions(TableOptions):
- def __init__(self, options=None):
- super(ModelTableOptions, self).__init__(options)
- self.model = getattr(options, 'model', None)
- self.columns = getattr(options, 'columns', None)
- self.exclude = getattr(options, 'exclude', None)
-
-
-def columns_for_model(model, columns=None, exclude=None):
- """
- Returns a ``SortedDict`` containing form columns for the given model.
-
- ``columns`` is an optional list of field names. If provided, only the
- named model fields will be included in the returned column list.
-
- ``exclude`` is an optional list of field names. If provided, the named
- model fields will be excluded from the returned list of columns, even
- if they are listed in the ``fields`` argument.
- """
-
- field_list = []
- opts = model._meta
- for f in opts.fields + opts.many_to_many:
- if (columns and not f.name in columns) or \
- (exclude and f.name in exclude):
- continue
- column = Column(verbose_name=f.verbose_name) # TODO: chose correct column type, with right options
- if column:
- field_list.append((f.name, column))
- field_dict = SortedDict(field_list)
- if columns:
- field_dict = SortedDict(
- [(c, field_dict.get(c)) for c in columns
- if ((not exclude) or (exclude and c not in exclude))]
- )
- return field_dict
-
-
-class BoundModelRow(BoundRow):
- """Special version of the BoundRow class that can handle model instances
- as data.
-
- We could simply have ModelTable spawn the normal BoundRow objects
- with the instance converted to a dict instead. However, this way allows
- us to support non-field attributes and methods on the model as well.
- """
-
- def _default_render(self, boundcol):
- """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('__')
- current = self.data
- for bit in bits:
- # note the difference between the attribute being None and not
- # existing at all; assume "value doesn't exist" in the former
- # (e.g. a relationship has no value), raise error in the latter.
- # a more proper solution perhaps would look at the model meta
- # data instead to find out whether a relationship is valid; see
- # 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))
-
- current = getattr(current, bit)
- 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
-
- if current is None:
- # ...the whole name (i.e. the last bit) resulted in None
- if boundcol.column.default is not None:
- return boundcol.get_default(self)
- return current
-
-
-class ModelRows(Rows):
- row_class = BoundModelRow
-
- def __init__(self, *args, **kwargs):
- super(ModelRows, self).__init__(*args, **kwargs)
-
- def _reset(self):
- self._length = None
-
- def __len__(self):
- """Use the queryset count() method to get the length, instead of
- loading all results into memory. This allows, for example,
- smart paginators that use len() to perform better.
- """
- if getattr(self, '_length', None) is None:
- self._length = self.table.data.count()
- return self._length
-
- # for compatibility with QuerySetPaginator
- count = __len__
-
-
-class ModelTableMetaclass(DeclarativeColumnsMetaclass):
- def __new__(cls, name, bases, attrs):
- # Let the default form meta class get the declared columns; store
- # those in a separate attribute so that ModelTable inheritance with
- # differing models works as expected (the behaviour known from
- # ModelForms).
- self = super(ModelTableMetaclass, cls).__new__(
- cls, name, bases, attrs, parent_cols_from='declared_columns')
- self.declared_columns = self.base_columns
-
- opts = self._meta = ModelTableOptions(getattr(self, 'Meta', None))
- # if a model is defined, then build a list of default columns and
- # let the declared columns override them.
- if opts.model:
- columns = columns_for_model(opts.model, opts.columns, opts.exclude)
- columns.update(self.declared_columns)
- self.base_columns = columns
- return self
-
-
-class ModelTable(BaseTable):
- """Table that is based on a model.
-
- Similar to ModelForm, a column will automatically be created for all
- the model's fields. You can modify this behaviour with a inner Meta
- class:
-
- class MyTable(ModelTable):
- class Meta:
- model = MyModel
- exclude = ['fields', 'to', 'exclude']
- columns = ['fields', 'to', 'include']
-
- One difference to a normal table is the initial data argument. It can
- be a queryset or a model (it's default manager will be used). If you
- 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 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()
- elif hasattr(data, '_default_manager'): # saves us db.models import
- self.queryset = data._default_manager.all()
- else:
- self.queryset = data
-
- 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."""
-
- # let the base class sort out the easy ones
- result = super(ModelTable, self)._validate_column_name(name, purpose)
- if not result:
- return False
-
- 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:
- 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
- with the necessary filters etc. attached.
- """
-
- # reset caches
- self._columns._reset()
- self._rows._reset()
-
- 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))
- return queryset
+"""Needed to make this package a Django app"""
+++ /dev/null
-"""Global module options.
-
-I'm not entirely happy about these existing at this point; maybe we can
-get rid of them.
-"""
-
-
-__all__ = ('options',)
-
-
-# A common use case is passing incoming query values directly into the
-# table constructor - data that can easily be invalid, say if manually
-# modified by a user. So by default, such errors will be silently
-# ignored. Set the option below to False if you want an exceptions to be
-# raised instead.
-class DefaultOptions(object):
- IGNORE_INVALID_OPTIONS = True
-options = DefaultOptions()
--- /dev/null
+class BoundRow(object):
+ """Represents a single row of in a table.
+
+ BoundRow provides a layer on top of the table data that exposes final
+ rendered cell values for the table. This means that formatting (via
+ Column.formatter or overridden Column.render in subclasses) applied to the
+ values from the table's data.
+ """
+ def __init__(self, table, data):
+ self.table = table
+ self.data = data
+
+ def __iter__(self):
+ for value in self.values:
+ yield value
+
+ 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(self.table, bound_column, self)
+
+ def __contains__(self, item):
+ """Check by both row object and column name."""
+ if isinstance(item, basestring):
+ return item in self.table._columns
+ else:
+ return item in self
+
+ @property
+ def values(self):
+ for column in self.table.columns:
+ yield self[column.name]
+
+
+class Rows(object):
+ """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
+ iterator in the table class.
+ """
+ def __init__(self, table):
+ self.table = table
+
+ def all(self):
+ """Return all rows."""
+ for row in self.table.data:
+ yield BoundRow(self.table, row)
+
+ def page(self):
+ """Return rows on current page (if paginated)."""
+ if not hasattr(self.table, 'page'):
+ return None
+ return iter(self.table.page.object_list)
+
+ def __iter__(self):
+ return iter(self.all())
+
+ def __len__(self):
+ return len(self.table.data)
+
+ # for compatibility with QuerySetPaginator
+ count = __len__
+
+ def __getitem__(self, key):
+ if isinstance(key, slice):
+ result = list()
+ for row in self.table.data[key]:
+ result.append(BoundRow(self.table, row))
+ return result
+ elif isinstance(key, int):
+ return BoundRow(self.table, self.table.data[key])
+ else:
+ raise TypeError('Key must be a slice or integer.')
--- /dev/null
+# -*- coding: utf8 -*-
+import copy
+from django.db.models.query import QuerySet
+from django.core.paginator import Paginator
+from django.utils.datastructures import SortedDict
+from django.http import Http404
+from django.template.loader import get_template
+from django.template import Context
+from .utils import rmprefix, toggleprefix, OrderByTuple, Accessor
+from .columns import Column
+from .memory import sort_table
+from .rows import Rows, BoundRow
+from .columns import Columns
+
+__all__ = ('Table',)
+
+QUERYSET_ACCESSOR_SEPARATOR = '__'
+
+class TableData(object):
+ """Exposes a consistent API for a table data. It currently supports a query
+ set and a list of dicts.
+ """
+ def __init__(self, data, table):
+ self._data = data if not isinstance(data, QuerySet) else None
+ self._queryset = data if isinstance(data, QuerySet) else None
+ self._table = table
+
+ # work with a copy of the data that has missing values populated with
+ # defaults.
+ if self._data:
+ self._data = copy.copy(self._data)
+ self._populate_missing_values(self._data)
+
+ def __len__(self):
+ # Use the queryset count() method to get the length, instead of
+ # loading all results into memory. This allows, for example,
+ # smart paginators that use len() to perform better.
+ return self._queryset.count() if self._queryset else len(self._data)
+
+ def order_by(self, order_by):
+ """Order the data based on column names in the table."""
+ # translate order_by to something suitable for this data
+ order_by = self._translate_order_by(order_by)
+ if self._queryset:
+ # need to convert the '.' separators to '__' (filter syntax)
+ order_by = order_by.replace(Accessor.SEPARATOR,
+ QUERYSET_ACCESSOR_SEPARATOR)
+ self._queryset = self._queryset.order_by(**order_by)
+ else:
+ self._data.sort(cmp=order_by.cmp)
+
+ def _translate_order_by(self, order_by):
+ """Translate from column names to column accessors"""
+ translated = []
+ for name in order_by:
+ # handle order prefix
+ prefix, name = ((name[0], name[1:]) if name[0] == '-'
+ else ('', name))
+ # find the accessor name
+ column = self._table.columns[name]
+ if not isinstance(column.accessor, basestring):
+ raise TypeError('unable to sort on a column that uses a '
+ 'callable accessor')
+ translated.append(prefix + column.accessor)
+ return OrderByTuple(translated)
+
+ def _populate_missing_values(self, data):
+ """Populates self._data with missing values based on the default value
+ for each column. It will create new items in the dataset (not modify
+ existing ones).
+ """
+ for i, item in enumerate(data):
+ # add data that is missing from the source. we do this now
+ # so that the column's ``default`` values can affect
+ # sorting (even when callables are used)!
+ #
+ # 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.
+ modified_item = None
+ for bound_column in self._table.columns.all():
+ # the following will be True if:
+ # * the source does not provide a value for the column
+ # or the value is None
+ # * the column did provide a data callable that
+ # returned None
+ accessor = Accessor(bound_column.accessor)
+ try:
+ if accessor.resolve(item) is None: # may raise ValueError
+ raise ValueError('None values also need replacing')
+ except ValueError:
+ if modified_item is None:
+ modified_item = copy.copy(item)
+ modified_item[accessor.bits[0]] = bound_column.default
+ if modified_item is not None:
+ data[i] = modified_item
+
+ def data_for_cell(self, bound_column, bound_row, apply_formatter=True):
+ """Calculate the value of a cell given a bound row and bound column.
+
+ *formatting* – Apply column formatter after retrieving the value from
+ the data.
+ """
+ value = Accessor(bound_column.accessor).resolve(bound_row.data)
+ # try and use default value if we've only got 'None'
+ if value is None and bound_column.default is not None:
+ value = bound_column.default()
+ if apply_formatter and bound_column.formatter:
+ value = bound_column.formatter(value)
+ return value
+
+ def __getitem__(self, key):
+ return self._data[key]
+
+
+class DeclarativeColumnsMetaclass(type):
+ """Metaclass that converts Column attributes on the class to a dictionary
+ called 'base_columns', taking into account parent class 'base_columns' as
+ well.
+ """
+ def __new__(cls, name, bases, attrs, parent_cols_from=None):
+ """The ``parent_cols_from`` argument determines from which attribute
+ we read the columns of a base class that this table might be
+ subclassing. This is useful for ``ModelTable`` (and possibly other
+ derivatives) which might want to differ between the declared columns
+ and others.
+
+ Note that if the attribute specified in ``parent_cols_from`` is not
+ found, we fall back to the default (``base_columns``), instead of
+ skipping over that base. This makes a table like the following work:
+
+ class MyNewTable(tables.ModelTable, MyNonModelTable):
+ pass
+
+ ``MyNewTable`` will be built by the ModelTable metaclass, which will
+ call this base with a modified ``parent_cols_from`` argument
+ specific to ModelTables. Since ``MyNonModelTable`` is not a
+ ModelTable, and thus does not provide that attribute, the columns
+ from that base class would otherwise be ignored.
+ """
+ # extract declared columns
+ columns = [(name, attrs.pop(name)) for name, column in attrs.items()
+ if isinstance(column, Column)]
+ columns.sort(lambda x, y: cmp(x[1].creation_counter,
+ y[1].creation_counter))
+
+ # If this class is subclassing other tables, add their fields as
+ # well. Note that we loop over the bases in *reverse* - this is
+ # necessary to preserve the correct order of columns.
+ for base in bases[::-1]:
+ cols_attr = (parent_cols_from if (parent_cols_from and
+ hasattr(base, parent_cols_from))
+ else 'base_columns')
+ if hasattr(base, cols_attr):
+ columns = getattr(base, cols_attr).items() + columns
+ # Note that we are reusing an existing ``base_columns`` attribute.
+ # This is because in certain inheritance cases (mixing normal and
+ # ModelTables) this metaclass might be executed twice, and we need
+ # to avoid overriding previous data (because we pop() from attrs,
+ # the second time around columns might not be registered again).
+ # An example would be:
+ # class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass
+ if not 'base_columns' in attrs:
+ attrs['base_columns'] = SortedDict()
+ attrs['base_columns'].update(SortedDict(columns))
+ attrs['_meta'] = TableOptions(attrs.get('Meta', None))
+ return type.__new__(cls, name, bases, attrs)
+
+
+class TableOptions(object):
+ def __init__(self, options=None):
+ super(TableOptions, self).__init__()
+ self.sortable = getattr(options, 'sortable', None)
+ self.order_by = getattr(options, 'order_by', ())
+
+
+class Table(object):
+ """A collection of columns, plus their associated data rows."""
+ __metaclass__ = DeclarativeColumnsMetaclass
+
+ # this value is not the same as None. it means 'use the default sort
+ # order', which may (or may not) be inherited from the table options.
+ # None means 'do not sort the data', ignoring the default.
+ DefaultOrder = type('DefaultSortType', (), {})()
+ TableDataClass = TableData
+
+ def __init__(self, data, order_by=DefaultOrder):
+ """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.
+
+ Note that unlike a ``Form``, tables are always bound to data. Also
+ unlike a form, the ``columns`` attribute is read-only and returns
+ ``BoundColumn`` wrappers, similar to the ``BoundField``s you get
+ when iterating over a form. This is because the table iterator
+ already yields rows, and we need an attribute via which to expose
+ the (visible) set of (bound) columns - ``Table.columns`` is simply
+ the perfect fit for this. Instead, ``base_colums`` is copied to
+ table instances, so modifying that will not touch the class-wide
+ column list.
+ """
+ self._rows = Rows(self) # bound rows
+ self._columns = Columns(self) # bound columns
+ self._data = self.TableDataClass(data=data, table=self)
+
+ # None is a valid order, so we must use DefaultOrder as a flag
+ # to fall back to the table sort order.
+ self.order_by = (self._meta.order_by if order_by is Table.DefaultOrder
+ else order_by)
+
+ # Make a copy so that modifying this will not touch the class
+ # definition. Note that this is different from forms, where the
+ # copy is made available in a ``fields`` attribute. See the
+ # ``Table`` class docstring for more information.
+ self.base_columns = copy.deepcopy(type(self).base_columns)
+
+ def __unicode__(self):
+ return self.as_html()
+
+ @property
+ def data(self):
+ return self._data
+
+ @property
+ def order_by(self):
+ return self._order_by
+
+ @order_by.setter
+ def order_by(self, value):
+ """Order the rows of the table based columns. ``value`` must be a
+ sequence of column names.
+ """
+ # accept both string and tuple instructions
+ order_by = value.split(',') if isinstance(value, basestring) else value
+ order_by = () if order_by is None else order_by
+ # validate, raise exception on failure
+ for o in order_by:
+ name = rmprefix(o)
+ if name not in self.columns:
+ raise ValueError('Column "%s" can not be used for ordering as '
+ 'it does not exist in the table' % name)
+ if not self.columns[name].sortable:
+ raise ValueError('Column "%s" can not be used for ordering as '
+ 'the column has explicitly forbidden it.' %
+ name)
+
+ new = OrderByTuple(order_by)
+ if not (hasattr(self, '_order_by') and self._order_by == new):
+ self._order_by = new
+ self._data.order_by(new)
+
+ @property
+ def rows(self):
+ return self._rows
+
+ @property
+ def columns(self):
+ return self._columns
+
+ def as_html(self):
+ """Render the table to a simple HTML table.
+
+ The rendered table won't include pagination or sorting, as those
+ features require a RequestContext. Use the ``render_table`` template
+ tag (requires ``{% load django_tables %}``) if you require this extra
+ functionality.
+ """
+ template = get_template('django_tables/basic_table.html')
+ return template.render(Context({'table': self}))
+
+ def paginate(self, klass=Paginator, page=1, *args, **kwargs):
+ self.paginator = klass(self.rows, *args, **kwargs)
+ try:
+ self.page = self.paginator.page(page)
+ except Exception as e:
+ raise Http404(str(e))
--- /dev/null
+{% spaceless %}
+<table>
+ <thead>
+ <tr>
+ {% for column in table.columns %}
+ <th>{{ column }}</th>
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for row in table.rows %}
+ <tr>
+ {% for value in row %}
+ <td>{{ value }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endspaceless %}
--- /dev/null
+{% load django_tables %}
+{% spaceless %}
+<table>
+ <thead>
+ <tr>
+ {% for column in table.columns %}
+ <th><a href="{% set_url_param sort=column.name_toggled %}">{{ column }}</a></th>
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for row in table.rows %}
+ <tr>
+ {% for cell in row %}
+ <td>{{ cell }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+</table>
+{% endspaceless %}
--- /dev/null
+"""
+Allows setting/changing/removing of chosen url query string parameters,
+while maintaining any existing others.
+
+Expects the current request to be available in the context as ``request``.
+
+Examples:
+
+ {% set_url_param page=next_page %}
+ {% set_url_param page="" %}
+ {% set_url_param filter="books" page=1 %}
+"""
+
+import urllib
+import tokenize
+import StringIO
+from django import template
+from django.template.loader import get_template
+from django.utils.safestring import mark_safe
+
+register = template.Library()
+
+
+class SetUrlParamNode(template.Node):
+ def __init__(self, changes):
+ self.changes = changes
+
+ def render(self, context):
+ request = context.get('request', None)
+ if not request:
+ return ""
+ # Note that we want params to **not** be a ``QueryDict`` (thus we
+ # don't use it's ``copy()`` method), as it would force all values
+ # to be unicode, and ``urllib.urlencode`` can't handle that.
+ params = dict(request.GET)
+ for key, newvalue in self.changes.items():
+ newvalue = newvalue.resolve(context)
+ if newvalue == '' or newvalue is None:
+ params.pop(key, False)
+ else:
+ params[key] = unicode(newvalue)
+
+ # ``urlencode`` chokes on unicode input, so convert everything to utf8.
+ # Note that if some query arguments passed to the site have their
+ # non-ascii characters screwed up when passed though this, it's most
+ # likely not our fault. Django (the ``QueryDict`` class to be exact)
+ # uses your projects DEFAULT_CHARSET to decode incoming query strings,
+ # whereas your browser might encode the url differently. For example,
+ # typing "ä" in my German Firefox's (v2) address bar results in "%E4"
+ # being passed to the server (in iso-8859-1), but Django might expect
+ # utf-8, where ä would be "%C3%A4"
+ def mkstr(s):
+ if isinstance(s, list):
+ return map(mkstr, s)
+ else:
+ return s.encode('utf-8') if isinstance(s, unicode) else s
+ params = dict([(mkstr(k), mkstr(v)) for k, v in params.items()])
+ # done, return (string is already safe)
+ return '?' + urllib.urlencode(params, doseq=True)
+
+
+def do_set_url_param(parser, token):
+ bits = token.contents.split()
+ qschanges = {}
+ for i in bits[1:]:
+ try:
+ a, b = i.split('=', 1)
+ a = a.strip()
+ b = b.strip()
+ a_line_iter = StringIO.StringIO(a).readline
+ keys = list(tokenize.generate_tokens(a_line_iter))
+ if keys[0][0] == tokenize.NAME:
+ # workaround bug #5270
+ b = (template.Variable(b) if b == '""' else
+ parser.compile_filter(b))
+ qschanges[str(a)] = b
+ else:
+ raise ValueError
+ except ValueError:
+ raise (template.TemplateSyntaxError,
+ "Argument syntax wrong: should be key=value")
+ return SetUrlParamNode(qschanges)
+
+register.tag('set_url_param', do_set_url_param)
+
+
+class RenderTableNode(template.Node):
+ def __init__(self, table_var_name):
+ self.table_var = template.Variable(table_var_name)
+
+ def render(self, context):
+ context = template.Context({
+ 'request': context.get('request', None),
+ 'table': self.table_var.resolve(context)
+ })
+ return get_template('django_tables/table.html').render(context)
+
+
+def do_render_table(parser, token):
+ try:
+ _, table_var_name = token.contents.split()
+ except ValueError:
+ raise template.TemplateSyntaxError,\
+ "%r tag requires a single argument" % token.contents.split()[0]
+ return RenderTableNode(table_var_name)
+
+register.tag('render_table', do_render_table)
--- /dev/null
+from attest import Tests
+from .core import core
+from .templates import templates
+#from .memory import memory
+#from .models import models
+
+tests = Tests([core, templates])
+
+def suite():
+ return tests.test_suite()
+
+if __name__ == '__main__':
+ tests.main()
--- /dev/null
+"""Test the core table functionality."""
+
+import copy
+from attest import Tests, Assert
+from django.http import Http404
+from django.core.paginator import Paginator
+import django_tables as tables
+from django_tables import utils
+
+core = Tests()
+
+
+@core.context
+def context():
+ class Context(object):
+ memory_data = [
+ {'i': 2, 'alpha': 'b', 'beta': 'b'},
+ {'i': 1, 'alpha': 'a', 'beta': 'c'},
+ {'i': 3, 'alpha': 'c', 'beta': 'a'},
+ ]
+
+ class UnsortedTable(tables.Table):
+ i = tables.Column()
+ alpha = tables.Column()
+ beta = tables.Column()
+
+ table = UnsortedTable(memory_data)
+
+ yield Context
+
+
+@core.test
+def declarations(context):
+ """Test defining tables by declaration."""
+ class GeoAreaTable(tables.Table):
+ name = tables.Column()
+ population = tables.Column()
+
+ assert len(GeoAreaTable.base_columns) == 2
+ assert 'name' in GeoAreaTable.base_columns
+ assert not hasattr(GeoAreaTable, 'name')
+
+ class CountryTable(GeoAreaTable):
+ capital = tables.Column()
+
+ assert len(CountryTable.base_columns) == 3
+ assert 'capital' in CountryTable.base_columns
+
+ # multiple inheritance
+ class AddedMixin(tables.Table):
+ added = tables.Column()
+
+ class CityTable(GeoAreaTable, AddedMixin):
+ mayor = tables.Column()
+
+ assert len(CityTable.base_columns) == 4
+ assert 'added' in CityTable.base_columns
+
+
+@core.test
+def datasource_untouched(context):
+ """Ensure that data that is provided to the table (the datasource) is not
+ modified by table operations.
+ """
+ original_data = copy.deepcopy(context.memory_data)
+
+ table = context.UnsortedTable(context.memory_data)
+ table.order_by = 'i'
+ list(table.rows)
+ assert context.memory_data == Assert(original_data)
+
+ table = context.UnsortedTable(context.memory_data)
+ table.order_by = 'beta'
+ list(table.rows)
+ assert context.memory_data == Assert(original_data)
+
+
+@core.test
+def sorting(context):
+ class MyUnsortedTable(tables.Table):
+ i = tables.Column()
+ alpha = tables.Column()
+ beta = tables.Column()
+
+ # various different ways to say the same thing: don't sort
+ Assert(MyUnsortedTable([]).order_by) == ()
+ Assert(MyUnsortedTable([], order_by=None).order_by) == ()
+ Assert(MyUnsortedTable([], order_by=[]).order_by) == ()
+ Assert(MyUnsortedTable([], order_by=()).order_by) == ()
+
+ # values of order_by are wrapped in tuples before being returned
+ Assert(MyUnsortedTable([], order_by='alpha').order_by) == ('alpha',)
+ Assert(MyUnsortedTable([], order_by=('beta',)).order_by) == ('beta',)
+
+ # a rewritten order_by is also wrapped
+ table = MyUnsortedTable([])
+ table.order_by = 'alpha'
+ assert ('alpha',) == table.order_by
+
+ # default sort order can be specified in table options
+ class MySortedTable(MyUnsortedTable):
+ class Meta:
+ order_by = 'alpha'
+
+ # order_by is inherited from the options if not explitly set
+ table = MySortedTable([])
+ assert ('alpha',) == table.order_by
+
+ # ...but can be overloaded at __init___
+ table = MySortedTable([], order_by='beta')
+ assert ('beta',) == table.order_by
+
+ # ...or rewritten later
+ table = MySortedTable(context.memory_data)
+ table.order_by = 'beta'
+ assert ('beta',) == table.order_by
+ assert 3 == table.rows[0]['i']
+
+ # ...or reset to None (unsorted), ignoring the table default
+ table = MySortedTable(context.memory_data, order_by=None)
+ assert () == table.order_by
+ assert 2 == table.rows[0]['i']
+
+
+@core.test
+def row_subscripting(context):
+ row = context.table.rows[0]
+ # attempt number indexing
+ Assert(row[0]) == 2
+ Assert(row[1]) == 'b'
+ Assert(row[2]) == 'b'
+ with Assert.raises(IndexError) as error:
+ row[3]
+ # attempt column name indexing
+ Assert(row['i']) == 2
+ Assert(row['alpha']) == 'b'
+ Assert(row['beta']) == 'b'
+ with Assert.raises(KeyError) as error:
+ row['gamma']
+
+
+@core.test
+def column_count(context):
+ class SimpleTable(tables.Table):
+ visible = tables.Column(visible=True)
+ hidden = tables.Column(visible=False)
+
+ # The columns container supports the len() builtin
+ assert len(SimpleTable([]).columns) == 1
+
+
+@core.test
+def column_accessor(context):
+ class SimpleTable(context.UnsortedTable):
+ col1 = tables.Column(accessor='alpha.upper.isupper')
+ col2 = tables.Column(accessor='alpha.upper')
+ table = SimpleTable(context.memory_data)
+ row = table.rows[0]
+ Assert(row['col1']) == True
+ Assert(row['col2']) == 'B'
+
+
+@core.test
+def pagination():
+ class BookTable(tables.Table):
+ name = tables.Column()
+
+ # create some sample data
+ data = []
+ for i in range(1,101):
+ data.append({'name': 'Book Nr. %d' % i})
+ books = BookTable(data)
+
+ # external paginator
+ paginator = Paginator(books.rows, 10)
+ assert paginator.num_pages == 10
+ page = paginator.page(1)
+ assert len(page.object_list) == 10
+ assert page.has_previous() == False
+ assert page.has_next() == True
+
+ # integrated paginator
+ books.paginate(Paginator, page=1, per_page=10)
+ # rows is now paginated
+ assert len(list(books.rows.page())) == 10
+ assert len(list(books.rows.all())) == 100
+ # new attributes
+ assert books.paginator.num_pages == 10
+ assert books.page.has_previous() == False
+ assert books.page.has_next() == True
+ # exceptions are converted into 404s
+ with Assert.raises(Http404) as error:
+ books.paginate(Paginator, page=9999, per_page=10)
+ books.paginate(Paginator, page='abc', per_page=10)
+
+
+@core.test
+def utilities():
+ assert utils.rmprefix('thing') == 'thing'
+ assert utils.rmprefix('-thing') == 'thing'
+ assert utils.toggleprefix('thing') == '-thing'
+ assert utils.toggleprefix('-thing') == 'thing'
+
+
+if __name__ == '__main__':
+ core.main()
"""
from math import sqrt
-from nose.tools import assert_raises
+from attest import Tests
from django.core.paginator import Paginator
import django_tables as tables
+memory = Tests()
-def test_basic():
- class StuffTable(tables.MemoryTable):
+@memory.test
+def basics():
+ class StuffTable(tables.Table):
name = tables.Column()
answer = tables.Column(default=42)
c = tables.Column(name="count", default=1)
email = tables.Column(data="@")
+
stuff = StuffTable([
{'id': 1, 'name': 'Foo Bar', '@': 'foo@bar.org'},
])
# columns with data= option work fine
assert r['email'] == 'foo@bar.org'
- # try to splice rows by index
- assert 'name' in stuff.rows[0]
- assert isinstance(stuff.rows[0:], list)
-
# [bug] splicing the table gives us valid, working rows
assert list(stuff[0]) == list(stuff.rows[0])
assert stuff[0]['name'] == 'Foo Bar'
Sets up a temporary Django project using a memory SQLite database.
"""
-from nose.tools import assert_raises, assert_equal
from django.conf import settings
from django.core.paginator import *
import django_tables as tables
+from attest import Tests
+models = Tests()
+'''
+
def setup_module(module):
settings.configure(**{
'DATABASE_ENGINE': 'sqlite3',
# reset
settings.DEBUG = False
+'''
--- /dev/null
+# -*- coding: utf8 -*-
+"""Test template specific functionality.
+
+Make sure tables expose their functionality to templates right. This
+generally about testing "out"-functionality of the tables, whether
+via templates or otherwise. Whether a test belongs here or, say, in
+``test_basic``, is not always a clear-cut decision.
+"""
+
+from django.template import Template, Context
+from django.http import HttpRequest
+import django_tables as tables
+from attest import Tests, Assert
+
+templates = Tests()
+
+
+@templates.context
+def context():
+ class Context(object):
+ class CountryTable(tables.Table):
+ name = tables.Column()
+ capital = tables.Column(sortable=False)
+ population = tables.Column(verbose_name='Population Size')
+ currency = tables.Column(visible=False)
+ tld = tables.Column(visible=False, verbose_name='Domain')
+ calling_code = tables.Column(accessor='cc',
+ verbose_name='Phone Ext.')
+
+ data = [
+ {'name': 'Germany', 'capital': 'Berlin', 'population': 83,
+ 'currency': 'Euro (€)', 'tld': 'de', 'cc': 49},
+ {'name': 'France', 'population': 64, 'currency': 'Euro (€)',
+ 'tld': 'fr', 'cc': 33},
+ {'name': 'Netherlands', 'capital': 'Amsterdam', 'cc': '31'},
+ {'name': 'Austria', 'cc': 43, 'currency': 'Euro (€)',
+ 'population': 8}
+ ]
+ yield Context
+
+
+@templates.test
+def as_html(context):
+ countries = context.CountryTable(context.data)
+ countries.as_html()
+
+
+@templates.test
+def custom_rendering(context):
+ """For good measure, render some actual templates."""
+ countries = context.CountryTable(context.data)
+ context = Context({'countries': countries})
+
+ # automatic and manual column verbose names
+ template = Template('{% for column in countries.columns %}{{ column }}/'
+ '{{ column.name }} {% endfor %}')
+ result = ('Name/name Capital/capital Population Size/population '
+ 'Phone Ext./calling_code ')
+ Assert(result) == template.render(context)
+
+ # 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 ')
+ Assert(result) == template.render(context)
+
+@templates.test
+def templatetag(context):
+ # ensure it works with a multi-order-by
+ countries = context.CountryTable(context.data,
+ order_by=('name', 'population'))
+ t = Template('{% load django_tables %}{% render_table table %}')
+ t.render(Context({'request': HttpRequest(), 'table': countries}))
--- /dev/null
+# -*- coding: utf-8 -*-
+from django.utils.datastructures import SortedDict
+from django.template import Context
+from django.utils.encoding import force_unicode, StrAndUnicode
+
+
+__all__ = ('BaseTable', 'options')
+
+
+def rmprefix(s):
+ """Normalize a column name by removing a potential sort prefix"""
+ return s[1:] if s[:1] == '-' else s
+
+
+def toggleprefix(s):
+ """Remove - prefix is existing, or add if missing."""
+ return s[1:] if s[:1] == '-' else '-' + s
+
+
+class OrderByTuple(tuple, StrAndUnicode):
+ """Stores 'order by' instructions; Used to render output in a format we
+ understand as input (see __unicode__) - especially useful in templates.
+
+ Also supports some functionality to interact with and modify the order.
+ """
+ def __unicode__(self):
+ """Output in our input format."""
+ return ','.join(self)
+
+ def __contains__(self, name):
+ """Determine whether a column is part of this order."""
+ for o in self:
+ if rmprefix(o) == name:
+ return True
+ return False
+
+ def is_reversed(self, name):
+ """Returns a bool indicating whether the column is ordered reversed,
+ None if it is missing.
+ """
+ for o in self:
+ if o == '-' + name:
+ return True
+ return False
+
+ def is_straight(self, name):
+ """The opposite of is_reversed."""
+ for o in self:
+ if o == name:
+ return True
+ return False
+
+ def polarize(self, reverse, names=()):
+ """Return a new tuple with the columns from ``names`` set to "reversed"
+ (e.g. prefixed with a '-'). Note that the name is ambiguous - do not
+ confuse this with ``toggle()``.
+
+ If names is not specified, all columns are reversed. If a column name
+ is given that is currently not part of the order, it is added.
+ """
+ prefix = '-' if reverse else ''
+ return OrderByTuple(
+ [o if (names and rmprefix(o) not in names)
+ else prefix + rmprefix(o) for o in self] +
+ [prefix + name for name in names if not name in self]
+ )
+
+ def toggle(self, names=()):
+ """Return a new tuple with the columns from ``names`` toggled with
+ respect to their "reversed" state. E.g. a '-' prefix will be removed is
+ existing, or added if lacking. Do not confuse with ``reverse()``.
+
+ If names is not specified, all columns are toggled. If a column name is
+ given that is currently not part of the order, it is added in
+ non-reverse form.
+ """
+ return OrderByTuple(
+ [o if (names and rmprefix(o) not in names)
+ else (o[1:] if o[:1] == '-' else '-' + o) for o in self] +
+ [name for name in names if not name in self]
+ )
+
+ @property
+ def cmp(self):
+ """Return a function suitable for sorting a list"""
+ def _cmp(a, b):
+ for accessor, reverse in instructions:
+ res = cmp(accessor.resolve(a), accessor.resolve(b))
+ if res != 0:
+ return -res if reverse else res
+ return 0
+ instructions = []
+ for o in self:
+ if o.startswith('-'):
+ instructions.append((Accessor(o[1:]), True))
+ else:
+ instructions.append((Accessor(o), False))
+ return _cmp
+
+
+class Accessor(object):
+ 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
+
+ @property
+ def bits(self):
+ return self.path.split(self.SEPARATOR)
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
- @echo " html to make standalone HTML files"
- @echo " dirhtml to make HTML files named index.html in directories"
- @echo " pickle to make pickle files"
- @echo " json to make JSON files"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " qthelp to make HTML files and a qthelp project"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " changes to make an overview of all changed/added/deprecated items"
- @echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-tables.qhc"
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/django-tables"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-tables"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
- @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
- "run these through (pdf)latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+++ /dev/null
-=================
-All about Columns
-=================
-
-Columns are what defines a table. Therefore, the way you configure your
-columns determines to a large extend how your table operates.
-
-``django_tables.columns`` currently defines three classes, ``Column``,
-``TextColumn`` and ``NumberColumn``. However, the two subclasses currently
-don't do anything special at all, so you can simply use the base class.
-While this will likely change in the future (e.g. when grouping is added),
-the base column class will continue to work by itself.
-
-There are no required arguments. The following is fine:
-
-.. code-block:: python
-
- class MyTable(tables.MemoryTable):
- c = tables.Column()
-
-It will result in a column named ``c`` in the table. You can specify the
-``name`` to override this:
-
-.. code-block:: python
-
- c = tables.Column(name="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``:
-
-.. 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:
-
-.. code-block:: python
-
- class BookTable(tables.ModelTable):
- book_name = tables.Column(name="name")
- author = tables.Column(data="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``.
-
-Apart from their internal name, you can define a string that will be used
-when for display via ``verbose_name``:
-
-.. code-block:: python
-
- pubdate = tables.Column(verbose_name="Published")
-
-The verbose name will be used, for example, if you put in a template:
-
-.. code-block:: django
-
- {{ column }}
-
-If you don't want a column to be sortable by the user:
-
-.. code-block:: python
-
- pubdate = tables.Column(sortable=False)
-
-Sorting is also affected by ``direction``, which can be used to change the
-*default* sort direction to descending. Note that this option only indirectly
-translates to the actual direction. Normal und reverse order, the terms
-django-tables exposes, now simply mean different things.
-
-.. code-block:: python
-
- pubdate = tables.Column(direction='desc')
-
-If you don't want to expose a column (but still require it to exist, for
-example because it should be sortable nonetheless):
-
-.. code-block:: python
-
- pubdate = tables.Column(visible=False)
-
-The column and it's values will now be skipped when iterating through the
-table, although it can still be accessed manually.
-
-Finally, you can specify default values for your columns:
-
-.. code-block:: python
-
- health_points = tables.Column(default=100)
-
-Note that how the default is used and when it is applied differs between
-table types.
# -*- coding: utf-8 -*-
#
# django-tables documentation build configuration file, created by
-# sphinx-quickstart on Fri Mar 26 08:40:14 2010.
+# sphinx-quickstart on Wed Jan 5 13:04:34 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.append(os.path.abspath('.'))
+#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc']
+extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
source_suffix = '.rst'
# The encoding of source files.
-#source_encoding = 'utf-8'
+#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'django-tables'
-copyright = u'2010, Michael Elsdörfer'
+#copyright = u''
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
-version = '0.1'
+version = '0.2'
# The full version, including alpha/beta/rc tags.
-release = '0.1'
+release = '0.2-dev'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
-# List of documents that shouldn't be included in the build.
-#unused_docs = []
-
-# List of directories, relative to source directory, that shouldn't be searched
-# for source files.
-exclude_trees = ['_build']
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# -- Options for HTML output ---------------------------------------------------
-# The theme to use for HTML and HTML Help pages. Major themes that come with
-# Sphinx are currently 'default' and 'sphinxdoc'.
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
#html_additional_pages = {}
# If false, no module index is generated.
-#html_use_modindex = True
+#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
-# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-tablesdoc'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index.rst', 'django-tables.tex', u'django-tables Documentation',
- u'Michael Elsdörfer', 'manual'),
+ ('index', 'django-tables.tex', u'django-tables Documentation',
+ u'n/a', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# not chapters.
#latex_use_parts = False
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
#latex_appendices = []
# If false, no module index is generated.
-#latex_use_modindex = True
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'django-tables', u'django-tables Documentation',
+ [u'n/a'], 1)
+]
+++ /dev/null
-===============
-How to do stuff
-===============
-
-This section will explain some specific features in more detail.
-
-.. toctree::
- :maxdepth: 1
-
- ordering
- pagination
+++ /dev/null
-=================
-Sorting the table
-=================
-
-``django-tables`` allows you to specify which column the user can sort,
-and will validate and resolve an incoming query string value the the
-correct ordering.
-
-It will also help you rendering the correct links to change the sort
-order in your template.
-
-
-Specify which columns are sortable
-----------------------------------
-
-Tables can take a ``sortable`` option through an inner ``Meta``, the same
-concept as known from forms and models in Django:
-
-.. code-block:: python
-
- class MyTable(tables.MemoryTable):
- class Meta:
- sortable = True
-
-This will be the default value for all columns, and it defaults to ``True``.
-You can override the table default for each individual column:
-
-.. code-block:: python
-
- class MyTable(tables.MemoryTable):
- foo = tables.Column(sortable=False)
- class Meta:
- sortable = True
-
-
-Setting the table ordering
---------------------------
-
-Your table both takes a ``order_by`` argument in it's constructor, and you
-can change the order by assigning to the respective attribute:
-
-.. code-block:: python
-
- table = MyTable(order_by='-foo')
- table.order_by = 'foo'
-
-You can see that the value expected is pretty much what is used by the
-Django database API: An iterable of column names, optionally using a hyphen
-as a prefix to indicate reverse order. However, you may also pass a
-comma-separated string:
-
-.. code-block:: python
-
- table = MyTable(order_by='column1,-column2')
-
-When you set ``order_by``, the value is parsed right away, and subsequent
-reads will give you the normalized value:
-
-.. code-block:: python
-
- >>> table.order_by = ='column1,-column2'
- >>> table.order_by
- ('column1', '-column2')
-
-Note: Random ordering is currently not supported.
-
-
-Error handling
-~~~~~~~~~~~~~~
-
-Passing incoming query string values from the request directly to the
-table constructor is a common thing to do. However, such data can easily
-contain invalid column names, be it that a user manually modified it,
-or someone put up a broken link. In those cases, you usually would not want
-to raise an exception (nor be notified by Django's error notification
-mechanism) - there is nothing you could do anyway.
-
-Because of this, such errors will by default be silently ignored. For
-example, if one out of three columns in an "order_by" is invalid, the other
-two will still be applied:
-
-.. code-block:: python
-
- >>> table.order_by = ('name', 'totallynotacolumn', '-date)
- >>> table.order_by
- ('name', '-date)
-
-This ensures that the following table will be created regardless of the
-value in ``sort``:
-
-.. code-block:: python
-
- table = MyTable(data, order_by=request.GET.get('sort'))
-
-However, if you want, you can disable this behaviour and have an exception
-raised instead, using:
-
-.. code-block:: python
-
- import django_tables
- django_tables.options.IGNORE_INVALID_OPTIONS = False
-
-
-Interacting with order
-----------------------
-
-Letting the user change the order of a table is a common scenario. With
-respect to Django, this means adding links to your table output that will
-send off the appropriate arguments to the server. ``django-tables``
-attempts to help with you that.
-
-A bound column, that is a column accessed through a table instance, provides
-the following attributes:
-
-- ``name_reversed`` will simply return the column name prefixed with a
- hyphen; this is useful in templates, where string concatenation can
- at times be difficult.
-
-- ``name_toggled`` checks the tables current order, and will then
- return the column either prefixed with an hyphen (for reverse ordering)
- or without, giving you the exact opposite order. If the column is
- currently not ordered, it will start off in non-reversed order.
-
-It is easy to be confused about the difference between the ``reverse`` and
-``toggle`` terminology. ``django-tables`` tries to put a normal/reverse-order
-abstraction on top of "ascending/descending", where as normal order could
-potentially mean either ascending or descending, depending on the column.
-
-Something you commonly see is a table that indicates which column it is
-currently ordered by through little arrows. To implement this, you will
-find useful:
-
-- ``is_ordered``: Returns ``True`` if the column is in the current
- ``order_by``, regardless of the polarity.
-
-- ``is_ordered_reverse``, ``is_ordered_straight``: Returns ``True`` if the
- column is ordered in reverse or non-reverse, respectively, otherwise
- ``False``.
-
-The above is usually enough for most simple cases, where tables are only
-ordered by a single column. For scenarios in which multi-column order is
-used, additional attributes are available:
-
-- ``order_by``: Return the current order, but with the current column
- set to normal ordering. If the current column is not already part of
- the order, it is appended. Any existing columns in the order are
- maintained as-is.
-
-- ``order_by_reversed``, ``order_by_toggled``: Similarly, return the
- table's current ``order_by`` with the column set to reversed or toggled,
- respectively. Again, it is appended if not already ordered.
-
-Additionally, ``table.order_by.toggle()`` may also be useful in some cases:
-It will toggle all order columns and should thus give you the exact
-opposite order.
-
-The following is a simple example of single-column ordering. It shows a list
-of sortable columns, each clickable, and an up/down arrow next to the one
-that is currently used to sort the table.
-
-.. code-block:: django
-
- Sort by:
- {% for column in table.columns %}
- {% if column.sortable %}
- <a href="?sort={{ column.name_toggled }}">{{ column }}</a>
- {% if column.is_ordered_straight %}<img src="down.png" />{% endif %}
- {% if column.is_ordered_reverse %}<img src="up.png" />{% endif %}
- {% endif %}
- {% endfor %}
+++ /dev/null
-----------
-Pagination
-----------
-
-If your table has a large number of rows, you probably want to paginate
-the output. There are two distinct approaches.
-
-First, you can just paginate over ``rows`` as you would do with any other
-data:
-
-.. code-block:: python
-
- table = MyTable(queryset)
- paginator = Paginator(table.rows, 10)
- page = paginator.page(1)
-
-You're not necessarily restricted to Django's own paginator (or subclasses) -
-any paginator should work with this approach, so long it only requires
-``rows`` to implement ``len()``, slicing, and, in the case of a
-``ModelTable``, a ``count()`` method. The latter means that the
-``QuerySetPaginator`` also works as expected.
-
-Alternatively, you may use the ``paginate`` feature:
-
-.. code-block:: python
-
- table = MyTable(queryset)
- table.paginate(Paginator, 10, page=1, orphans=2)
- for row in table.rows.page():
- pass
- table.paginator # new attributes
- table.page
-
-The table will automatically create an instance of ``Paginator``,
-passing it's own data as the first argument and additionally any arguments
-you have specified, except for ``page``. You may use any paginator, as long
-as it follows the Django protocol:
-
-* Take data as first argument.
-* Support a page() method returning an object with an ``object_list``
- attribute, exposing the paginated data.
-
-Note that due to the abstraction layer that ``django-tables`` represents, it
-is not necessary to use Django's ``QuerySetPaginator`` with model tables.
-Since the table knows that it holds a queryset, it will automatically choose
-to use count() to determine the data length (which is exactly what
-``QuerySetPaginator`` would do).
-==========================================
-django-tables - A Django Queryset renderer
-==========================================
+=====================================================
+django-tables - An app for creating HTML tables
+=====================================================
+django-tables simplifies the task of turning sets of datainto HTML tables. It
+has native support for pagination and sorting. It does for HTML tables what
+``django.forms`` does for HTML forms.
-``django-tables`` wants to help you present data while allowing your user
-to apply common tabular transformations on it.
+Quick start guide
+=================
-Currently, this mostly mostly means "sorting", i.e. parsing a query string
-coming from the browser (while supporting multiple sort fields, restricting
-the fields that may be sorted, exposing fields under different names) and
-generating the proper links to allow the user to change the sort order.
+1. Download and install the package.
+2. Install the tables framework by adding ``'django_tables'`` to your
+ ``INSTALLED_APPS`` setting.
+3. Ensure that ``'django.core.context_processors.request'`` is in your
+ ``TEMPLATE_CONTEXT_PROCESSORS`` setting.
+4. Write table classes for the types of tables you want to display.
+5. Create an instance of a table in a view, provide it your data, and pass it
+ to a template for display.
+6. Use ``{{ table.as_html }}``, the
+ :ref:`template tag <template_tags.render_table>`, or your own
+ custom template code to display the table.
-In the future, filtering and grouping will hopefully be added.
+Tables
+======
-A simple example
-----------------
+For each type of table you want to display, you will need to create a subclass
+of ``django_tables.Table`` that describes the structure of the table.
+
+In this example we are going to take some data describing three countries and
+turn it into a HTML table. We start by creating our data:
+
+.. code-block:: python
+
+ >>> countries = [
+ ... {'name': 'Australia', 'population': 21, 'tz': 'UTC +10', 'visits': 1},
+ ... {'name': 'Germany', 'population', 81, 'tz': 'UTC +1', 'visits': 2},
+ ... {'name': 'Mexico', 'population': 107, 'tz': 'UTC -6', 'visits': 0},
+ ... ]
-The API looks similar to that of Django's ``ModelForms``:
+Next we subclass ``django_tables.Table`` to create a table that describes our
+data. The API should look very familiar since it's based on Django's
+database model API:
.. code-block:: python
- import django_tables as tables
+ >>> import django_tables as tables
+ >>> class CountryTable(tables.Table):
+ ... name = tables.Column()
+ ... population = tables.Column()
+ ... tz = tables.Column(verbose_name='Time Zone')
+ ... visits = tables.Column()
+
+See :ref:`columns` for more information.
- 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")
-Instead of fields, you declare a column for every piece of data you want
-to expose to the user.
+Providing data
+--------------
-To use the table, create an instance:
+To use the table, simply create an instance of the table class and pass in your
+data. e.g. following on from the above example:
.. code-block:: python
- countries = CountryTable([{'name': 'Germany', population: 80},
- {'name': 'France', population: 64}])
+ >>> table = CountryTable(countries)
-Decide how the table should be sorted:
+Tables have support for any iterable data that contains objects with
+attributes that can be accessed as property or dictionary syntax:
.. code-block:: python
- countries.order_by = ('name',)
- assert [row.name for row in countries.row] == ['France', 'Germany']
+ >>> table = SomeTable([{'a': 1, 'b': 2}, {'a': 4, 'b': 8}]) # valid
+ >>> table = SomeTable(SomeModel.objects.all()) # also valid
- countries.order_by = ('-population',)
- assert [row.name for row in countries.row] == ['Germany', 'France']
+Each item in the data corresponds to one row in the table. By default, the
+table uses column names as the keys (or attributes) for extracting cell values
+from the data. This can be changed by using the :attr:`~Column.accessor`
+argument.
-If you pass the table object along into a template, you can do:
+
+Displaying a table
+------------------
+
+There are two ways to display a table, the easiest way is to use the table's
+own ``as_html`` method:
.. code-block:: django
- {% for column in countries.columns %}
- {{ column }}
- {% endfor %}
+ {{ table.as_html }}
+
+Which will render something like:
-Which will give you:
++--------------+------------+---------+
+| Country Name | Population | Tz |
++==============+============+=========+
+| Australia | 21 | UTC +10 |
++--------------+------------+---------+
+| Germany | 81 | UTC +1 |
++--------------+------------+---------+
+| Mexico | 107 | UTC -6 |
++--------------+------------+---------+
+
+The downside of this approach is that pagination and sorting will not be
+available. These features require the use of the ``{% render_table %}``
+template tag:
.. code-block:: django
- Country Name
- Timezone
+ {% load django_tables %}
+ {% render_table table %}
-Note that ``population`` is skipped (as it has ``visible=False``), that the
-declared verbose name for the ``name`` column is used, and that ``time_zone``
-is converted into a more beautiful string for output automatically.
+See :ref:`template tags` for more information.
-Common Workflow
-~~~~~~~~~~~~~~~
+Ordering
+--------
-Usually, you are going to use a table like this. Assuming ``CountryTable``
-is defined as above, your view will create an instance and pass it to the
-template:
+Controlling the order that the rows are displayed (sorting) is simple, just use
+the :attr:`~Table.order_by` property or pass it in when initialising the
+instance:
.. code-block:: python
- def list_countries(request):
- data = ...
- countries = CountryTable(data, order_by=request.GET.get('sort'))
- return render_to_response('list.html', {'table': countries})
+ >>> # order_by argument when creating table instances
+ >>> table = CountryTable(countries, order_by='name, -population')
+ >>> table = CountryTable(countries, order_by=('name', '-population'))
+ >>> # order_by property on table instances
+ >>> table = CountryTable(countries)
+ >>> table.order_by = 'name, -population'
+ >>> table.order_by = ('name', '-population')
+
-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:
+Customising the output
+======================
- * ``population`` has ``sortable=False``
- * ``time_zone`` has it's name overwritten with ``tz``.
+There are a number of options available for changing the way the table is
+rendered. Each approach provides balance of ease-of-use and control (the more
+control you want, the less easy it is to use).
-Then, in the ``list.html`` template, write:
+
+Column formatter
+----------------
+
+If all you want to do is change the way a column is formatted, you can simply
+provide the :attr:`~Column.formatter` argument to a :class:`Column` when you
+define the :class:`Table`:
+
+.. code-block:: python
+
+ >>> import django_tables as tables
+ >>> class SimpleTable(tables.Table):
+ ... id = tables.Column(formatter=lambda x: '#%d' % x)
+ ... age = tables.Column(formatter=lambda x: '%d years old' % x)
+ ...
+ >>> table = SimpleTable([{'age': 31, 'id': 10}, {'age': 34, 'id': 11}])
+ >>> row = table.rows[0]
+ >>> for cell in row:
+ ... print cell
+ ...
+ #10
+ 31 years old
+
+The limitation of this approach is that you're unable to incorporate any
+run-time information of the table into the formatter. For example it would not
+be possible to incorporate the row number into the cell's value.
+
+
+Column render method
+--------------------
+
+This approach provides a lot of control, but is only suitable if you intend to
+customise the rendering for a single table (otherwise you'll end up having to
+copy & paste the method to every table you want to modify – which violates
+DRY).
+
+ >>> import django_tables as tables
+ >>> class SimpleTable(tables.Table):
+ ... row_number = tables.Column()
+ ... id = tables.Column(formatter=lambda x: '#%d' % x)
+ ... age = tables.Column(formatter=lambda x: '%d years old' % x)
+ ...
+ ... def render_row_number(self, bound_column, bound_row):
+ ... value =
+ ...
+ ... def render_id(self, bound_column, bound_row):
+ ... value = self.column.
+ ...
+ >>> table = SimpleTable([{'age': 31, 'id': 10}, {'age': 34, 'id': 11}])
+ >>> row = table.rows[0]
+ >>> for cell in row:
+ ... print cell
+ ...
+ #10
+ 31 years old
+
+If you want full control over the way the table is rendered, create
+and render the template yourself:
.. code-block:: django
+ {% load django_tables %}
<table>
- <tr>
- {% for column in table.columns %}
- <th><a href="?sort={{ column.name_toggled }}">{{ column }}</a></th>
- {% endfor %}
- </tr>
- {% for row in table.rows %}
- <tr>
- {% for value in row %}
- <td>{{ value }}</td>
- {% endfor %}
- </tr>
- {% endfor %}
+ <thead>
+ <tr>
+ {% for column in table.columns %}
+ <th><a href="{% set_url_param sort=column.name_toggled %}">{{ column }}</a></th>
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for row in table.rows %}
+ <tr>
+ {% for cell in row %}
+ <td>{{ cell }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
</table>
-This will output the data as an HTML table. Note how the table is now fully
-sortable, since our link passes along the column name via the querystring,
-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
-structure to access columns directly:
+
+Columns
+=======
+
+The :class:`Columns` class provides an container for :class:`BoundColumn`
+instances. The simplest way to access the contained columns is to iterate over
+the instance:
+
+Each :class:`Table` instance has an instance as its :attr:`~Table.columns`
+property. Iterating over the instance yields only the visible columns. To
+access all columns (including those that are hidden), use the
+:func:`~Columns.all` method.
+
+Additionally, the :func:`~Columns.sortable` method provides access to all the
+sortable columns.
+
+
+Column options
+--------------
+
+Each column takes a certain set of column-specific arguments (documented in the
+:ref:`column reference <columns.types>`).
+
+There's also a set of common arguments available to all column types. All are
+optional. Here's a summary of them.
+
+ :attr:`~Column.verbose_name`
+ A pretty human readable version of the column name. Typically this is
+ used in the header cells in the HTML output.
+
+ :attr:`~Column.accessor`
+ A string or callable that specifies the attribute to access when
+ retrieving the value for a cell in this column from the data-set.
+ Multiple lookups can be achieved by providing a dot separated list of
+ lookups, e.g. ``"user.first_name"``. The functionality is identical to
+ that of Django's template variable syntax, e.g. ``{{ user.first_name
+ }}``
+
+ A callable should be used if the dot separated syntax is not capable of
+ describing the lookup properly. The callable will be passed a single
+ item from the data (if the table is using :class:`QuerySet` data, this
+ would be a :class:`Model` instance), and is expected to return the
+ correct value for the column.
+
+ Consider the following:
+
+ .. code-block:: python
+
+ >>> import django_tables as tables
+ >>> data = [
+ ... {'dot.separated.key': 1},
+ ... {'dot.separated.key': 2},
+ ... ]
+ ...
+ >>> class SlightlyComplexTable(tables.Table):
+ >>> dot_seperated_key = tables.Column(accessor=lambda x: x['dot.separated.key'])
+ ...
+ >>> table = SlightlyComplexTable(data)
+ >>> for row in table.rows:
+ >>> print row['dot_seperated_key']
+ ...
+ 1
+ 2
+
+ This would not have worked:
+
+ .. code-block:: python
+
+ dot_seperated_key = tables.Column(accessor='dot.separated.key')
+
+ :attr:`~Column.default`
+ The default value for the column. This can be a value or a callable
+ object [1]_. If an object in the data provides :const:`None` for a
+ column, the default will be used instead.
+
+ The default value may affect ordering, depending on the type of
+ data the table is using. The only case where ordering is not affected
+ ing when a :class:`QuerySet` is used as the table data (since sorting
+ is performed by the database).
+
+ .. [1] The provided callable object must not expect to receive any
+ arguments.
+
+ :attr:`~Column.visible`
+ If :const:`False`, this column will not be in the HTML output.
+
+ When a field is not visible, it is removed from the table's
+ :attr:`~Column.columns` iterable.
+
+ :attr:`~Column.sortable`
+ If :const:`False`, this column will not be allowed to be used in
+ ordering the table.
+
+ :attr:`~Column.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.
+
+
+Rows
+====
+
+Row objects
+-----------
+
+A row object represents a single row in a table.
+
+To access the rendered value of each cell in a row, you can iterate over the
+row:
+
+.. code-block:: python
+
+ >>> import django_tables as tables
+ >>> class SimpleTable(tables.Table):
+ ... a = tables.Column()
+ ... b = tables.CheckBoxColumn(attrs={'name': 'my_chkbox'})
+ ...
+ >>> table = SimpleTable([{'a': 1, 'b': 2}])
+ >>> row = table.rows[0] # we only have one row, so let's use it
+ >>> for cell in row:
+ ... print cell
+ ...
+ 1
+ <input type="checkbox" name="my_chkbox" value="2" />
+
+Alternatively you can treat it like a list and use indexing to retrieve a
+specific cell. It should be noted that this will raise an IndexError on
+failure.
+
+.. code-block:: python
+
+ >>> row[0]
+ 1
+ >>> row[1]
+ u'<input type="checkbox" name="my_chkbox" value="2" />'
+ >>> row[2]
+ ...
+ IndexError: list index out of range
+
+Finally you can also treat it like a dictionary and use column names as the
+keys. This will raise KeyError on failure (unlike the above indexing using
+integers).
+
+.. code-block:: python
+
+ >>> row['a']
+ 1
+ >>> row['b']
+ u'<input type="checkbox" name="my_chkbox" value="2" />'
+ >>> row['c']
+ ...
+ KeyError: 'c'
+
+
+
+Template tags
+=============
+
+.. _template_tags.render_table:
+
+render_table
+------------
+
+If you want to render a table that provides support for sorting and pagination,
+you must use the ``{% render_table %}`` template tag. In this example ``table``
+is an instance of a :class:`django_tables.Table` that has been put into the
+template context:
+
+.. code-block:: django
+
+ {% load django_tables %}
+ {% render_table table %}
+
+
+.. _template_tags.set_url_param:
+
+set_url_param
+-------------
+
+This template tag is a utility that allows you to update a portion of the
+query-string without overwriting the entire thing. However you shouldn't need
+to use this template tag unless you are rendering the table from scratch (i.e.
+not using ``as_html()`` or ``{% render_table %}``).
+
+This is very useful if you want the give your users the ability to interact
+with your table (e.g. change the ordering), because you will need to create
+urls with the appropriate queries.
+
+Let's assume we have the query-string
+``?search=pirates&sort=name&page=5`` and we want to update the ``sort``
+parameter:
.. code-block:: django
- {% if table.columns.tz.visible %}
- {{ table.columns.tz }}
- {% endfor %}
+ {% set_url_param sort="dob" %} # ?search=pirates&sort=dob&page=5
+ {% set_url_param sort="" %} # ?search=pirates&page=5
+ {% set_url_param sort="" search="" %} # ?page=5
-In Detail
-=========
-.. toctree::
- :maxdepth: 2
+A table instance bound to data has two attributes ``columns`` and ``rows``,
+which can be iterated over:
- installation
- types/index
- features/index
- columns
- templates
+.. code-block:: django
+
+ <table>
+ <thead>
+ <tr>
+ {% for column in table.columns %}
+ <th><a href="?sort={{ column.name_toggled }}">{{ column }}</a></th>
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for row in table.rows %}
+ <tr>
+ {% for value in row %}
+ <td>{{ value }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
-Indices and tables
-==================
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
+Custom render methods
+---------------------
+
+Often, displaying a raw value of a table cell is not good enough. For
+example, if your table has a ``rating`` column, you might want to show
+an image showing the given number of **stars**, rather than the plain
+numeric value.
+
+While you can always write your templates so that the column in question
+is treated separately, either by conditionally checking for a column name,
+or by explicitely rendering each column manually (as opposed to simply
+looping over the ``rows`` and ``columns`` attributes), this is often
+tedious to do.
+
+Instead, you can opt to move certain formatting responsibilites into
+your Python code:
+
+.. code-block:: python
+ class BookTable(tables.ModelTable):
+ name = tables.Column()
+ rating = tables.Column(accessor='rating_int')
+
+ def render_rating(self, bound_table):
+ if bound_table.rating_count == 0:
+ return '<img src="no-rating.png"/>'
+ else:
+ return '<img src="rating-%s.png"/>' % bound_table.rating_int
+
+When accessing ``table.rows[i].rating``, the ``render_rating`` 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 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.
+++ /dev/null
-------------
-Installation
-------------
-
-Adding ``django-tables`` to your ``INSTALLED_APPS`` setting is optional.
-It'll get you the ability to load some template utilities via
-``{% load tables %}``, but apart from that,
-``import django_tables as tables`` should get you going.
-
-
-Running the test suite
-----------------------
-
-The test suite uses nose:
- http://somethingaboutorange.com/mrl/projects/nose/
REM Command file for Sphinx documentation
-set SPHINXBUILD=sphinx-build
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
- echo. html to make standalone HTML files
- echo. dirhtml to make HTML files named index.html in directories
- echo. pickle to make pickle files
- echo. json to make JSON files
- echo. htmlhelp to make HTML files and a HTML help project
- echo. qthelp to make HTML files and a qthelp project
- echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
- echo. changes to make an overview over all changed/added/deprecated items
- echo. linkcheck to check all external links for integrity
- echo. doctest to run all doctests embedded in the documentation if enabled
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
goto end
)
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
+++ /dev/null
-===================
-Rendering the table
-===================
-
-A table instance bound to data has two attributes ``columns`` and ``rows``,
-which can be iterate over:
-
-.. code-block:: django
-
- <table>
- <tr>
- {% for column in table.columns %}
- <th><a href="?sort={{ column.name_toggled }}">{{ column }}</a></th>
- {% endfor %}
- </tr>
- {% for row in table.rows %}
- <tr>
- {% for value in row %}
- <td>{{ value }}</td>
- {% endfor %}
- </tr>
- {% endfor %}
- </table>
-
-For the attributes available on a bound column, see :doc:`features/index`,
-depending on what you want to accomplish.
-
-
-Custom render methods
----------------------
-
-Often, displaying a raw value of a table cell is not good enough. For
-example, if your table has a ``rating`` column, you might want to show
-an image showing the given number of **stars**, rather than the plain
-numeric value.
-
-While you can always write your templates so that the column in question
-is treated separately, either by conditionally checking for a column name,
-or by explicitely rendering each column manually (as opposed to simply
-looping over the ``rows`` and ``columns`` attributes), this is often
-tedious to do.
-
-Instead, you can opt to move certain formatting responsibilites into
-your Python code:
-
-.. code-block:: django
-
- class BookTable(tables.ModelTable):
- name = tables.Column()
- rating_int = tables.Column(name="rating")
-
- def render_rating(self, instance):
- if instance.rating_count == 0:
- return '<img ="no-rating.png">'
- else:
- return '<img ="rating-%s.png">' % instance.rating_int
-
-When accessing ``table.rows[i].rating``, the ``render_rating`` 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 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.
-
-
-The table.columns container
----------------------------
-
-While you can iterate through the ``columns`` attribute and get all the
-currently visible columns, it further provides features that go beyond
-a simple iterator.
-
-You can access all columns, regardless of their visibility, through
-``columns.all``.
-
-``columns.sortable`` is a handy shortcut that exposes all columns which's
-``sortable`` attribute is True. This can be very useful in templates, when
-doing {% if column.sortable %} can conflict with {{ forloop.last }}.
-
-
-Template Utilities
-------------------
-
-If you want the give your users the ability to interact with your table (e.g.
-change the ordering), you will need to create urls with the appropriate
-queries. To simplify that process, django-tables comes with a helpful
-templatetag:
-
-.. code-block:: django
-
- {% set_url_param sort="name" %} # ?sort=name
- {% set_url_param sort="" %} # delete "sort" param
-
-The template library can be found in 'django_modules.app.templates.tables'.
-If you add ''django_modules.app' to your ``INSTALLED_APPS`` setting, you
-will be able to do:
-
-.. code-block:: django
-
- {% load tables %}
-
-Note: The tag requires the current request to be available as ``request``
-in the context (usually, this means activating the Django request context
-processor).
+++ /dev/null
-===========
-Table types
-===========
-
-Different types of tables are available:
-
-.. toctree::
- :maxdepth: 1
-
- MemoryTable - uses dicts as the data source <memory>
- ModelTable - wraps around a Django Model <models>
- SqlTable - is based on a raw SQL query <sql>
+++ /dev/null
------------
-MemoryTable
------------
-
-This table expects an iterable of ``dict`` (or compatible) objects as the
-data source. Values found in the data that are not associated with a column
-are ignored, missing values are replaced by the column default or ``None``.
-
-Sorting is done in memory, in pure Python.
-
-Dynamic Data
-~~~~~~~~~~~~
-
-If any value in the source data is a callable, it will be passed it's own
-row instance and is expected to return the actual value for this particular
-table cell.
-
-Similarily, the colunn default value may also be callable that will take
-the row instance as an argument (representing the row that the default is
-needed for).
+++ /dev/null
-----------
-ModelTable
-----------
-
-This table type is based on a Django model. It will use the Model's data,
-and, like a ``ModelForm``, can automatically generate it's columns based
-on the mode fields.
-
-.. code-block:: python
-
- class CountryTable(tables.ModelTable):
- id = tables.Column(sortable=False, visible=False)
- class Meta:
- model = Country
- exclude = ['clicks']
-
-In this example, the table will have one column for each model field,
-with the exception of ``clicks``, which is excluded. The column for ``id``
-is overwritten to both hide it by default and deny it sort capability.
-
-When instantiating a ``ModelTable``, you usually pass it a queryset to
-provide the table data:
-
-.. code-block:: python
-
- qs = Country.objects.filter(continent="europe")
- countries = CountryTable(qs)
-
-However, you can also just do:
-
-.. code-block:: python
-
- countries = CountryTable()
-
-and all rows exposed by the default manager of the model the table is based
-on will be used.
-
-If you are using model inheritance, then the following also works:
-
-.. code-block:: python
-
- countries = CountryTable(CountrySubclass)
-
-Note that while you can pass any model, it really only makes sense if the
-model also provides fields for the columns you have defined.
-
-If you just want to use a ``ModelTable``, but without auto-generated
-columns, you do not have to list all model fields in the ``exclude``
-``Meta`` option. Instead, simply don't specify a model.
-
-
-Custom Columns
-~~~~~~~~~~~~~~
-
-You an add custom columns to your ModelTable that are not based on actual
-model fields:
-
-.. code-block:: python
-
- class CountryTable(tables.ModelTable):
- custom = tables.Column(default="foo")
- class Meta:
- model = Country
-
-Just make sure your model objects do provide an attribute with that name.
-Functions are also supported, so ``Country.custom`` could be a callable.
-
-
-Spanning relationships
-~~~~~~~~~~~~~~~~~~~~~~
-
-Let's assume you have a ``Country`` model, with a ``ForeignKey`` ``capital``
-pointing to the ``City`` model. While displaying a list of countries,
-you might want want to link to the capital's geographic location, which is
-stored in ``City.geo`` as a ``(lat, long)`` tuple, on, say, a Google Map.
-
-``ModelTable`` supports the relationship spanning syntax of Django's
-database API:
-
-.. code-block:: python
-
- class CountryTable(tables.ModelTable):
- city__geo = tables.Column(name="geo")
-
-This will add a column named "geo", based on the field by the same name
-from the "city" relationship. Note that the name used to define the column
-is what will be used to access the data, while the name-overwrite passed to
-the column constructor just defines a prettier name for us to work with.
-This is to be consistent with auto-generated columns based on model fields,
-where the field/column name naturally equals the source name.
-
-However, to make table defintions more visually appealing and easier to
-read, an alternative syntax is supported: setting the column ``data``
-property to the appropriate string.
-
-.. code-block:: python
-
- class CountryTable(tables.ModelTable):
- geo = tables.Column(data='city__geo')
-
-Note that you don't need to define a relationship's fields as separate
-columns if you already have a column for the relationship itself, i.e.:
-
-.. code-block:: python
-
- class CountryTable(tables.ModelTable):
- city = tables.Column()
-
- for country in countries.rows:
- print country.city.id
- print country.city.geo
- print country.city.founder.name
-
-
-``ModelTable`` Specialties
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-``ModelTable`` currently has some restrictions with respect to ordering:
-
-* Custom columns not based on a model field do not support ordering,
- regardless of the ``sortable`` property (it is ignored).
-
-* A ``ModelTable`` column's ``default`` or ``data`` value does not affect
- ordering. This differs from the non-model table behaviour.
-
-If a column is mapped to a method on the model, that method will be called
-without arguments. This behavior differs from memory tables, where a
-row object will be passed.
-
-If you are using callables (e.g. for the ``default`` or ``data`` column
-options), they will generally be run when a row is accessed, and
-possible repeatedly when accessed more than once. This behavior differs from
-memory tables, where they would be called once, when the table is
-generated.
+++ /dev/null
---------
-SqlTable
---------
-
-This table is backed by an SQL query that you specified. It'll help you
-ensure that pagination and sorting options are properly reflected in the
-query.
-
-**Currently not implemented yet.**
+++ /dev/null
-django
-nose
-Sphinx
-#!/usr/bin/env python
-import os
+# -*- coding: utf8 -*-
from distutils.core import setup
+from distutils.command.install_data import install_data
+from distutils.command.install import INSTALL_SCHEMES
+import os
+import sys
+
+class osx_install_data(install_data):
+ # On MacOS, the platform-specific lib dir is /System/Library/Framework/Python/.../
+ # which is wrong. Python 2.5 supplied with MacOS 10.5 has an Apple-specific fix
+ # for this in distutils.command.install_data#306. It fixes install_lib but not
+ # install_data, which is why we roll our own install_data class.
+ def finalize_options(self):
+ # By the time finalize_options is called, install.install_lib is set to the
+ # fixed directory, so we set the installdir to install_lib. The
+ # install_data class uses ('install_data', 'install_dir') instead.
+ self.set_undefined_options('install', ('install_lib', 'install_dir'))
+ install_data.finalize_options(self)
-# Figure out the version; this could be done by importing the
-# module, though that requires Django to be already installed,
-# which may not be the case when processing a pip requirements
-# file, for example.
-import re
-here = os.path.dirname(os.path.abspath(__file__))
-version_re = re.compile(
- r'__version__ = (\(.*?\))')
-fp = open(os.path.join(here, 'django_tables', '__init__.py'))
-version = None
-for line in fp:
- match = version_re.search(line)
- if match:
- version = eval(match.group(1))
- break
+if sys.platform == "darwin":
+ cmdclasses = {'install_data': osx_install_data}
else:
- raise Exception("Cannot find version in __init__.py")
-fp.close()
+ cmdclasses = {'install_data': install_data}
+
+def fullsplit(path, result=None):
+ """
+ Split a pathname into components (the opposite of os.path.join) in a
+ platform-neutral way.
+ """
+ if result is None:
+ result = []
+ head, tail = os.path.split(path)
+ if head == '':
+ return [tail] + result
+ if head == path:
+ return result
+ return fullsplit(head, [tail] + result)
+
+# Tell distutils to put the data_files in platform-specific installation
+# locations. See here for an explanation:
+# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb
+for scheme in INSTALL_SCHEMES.values():
+ scheme['data'] = scheme['purelib']
+# Compile the list of packages available, because distutils doesn't have
+# an easy way to do this.
+packages, data_files = [], []
+root_dir = os.path.dirname(__file__)
+if root_dir != '':
+ os.chdir(root_dir)
+package_dir = 'django_tables'
-def find_packages(root):
- # so we don't depend on setuptools; from the Storm ORM setup.py
- packages = []
- for directory, subdirectories, files in os.walk(root):
- if '__init__.py' in files:
- packages.append(directory.replace(os.sep, '.'))
- return packages
+for dirpath, dirnames, filenames in os.walk(package_dir):
+ # Ignore dirnames that start with '.'
+ for i, dirname in enumerate(dirnames):
+ if dirname.startswith('.'): del dirnames[i]
+ if '__init__.py' in filenames:
+ packages.append('.'.join(fullsplit(dirpath)))
+ elif filenames:
+ data_files.append([dirpath, [os.path.join(dirpath, f) for f in filenames]])
+# Small hack for working with bdist_wininst.
+# See http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html
+if len(sys.argv) > 1 and sys.argv[1] == 'bdist_wininst':
+ for file_info in data_files:
+ file_info[0] = '\\PURELIB\\%s' % file_info[0]
setup(
name = 'django-tables',
- version=".".join(map(str, version)),
- description = 'Render QuerySets as tabular data in Django.',
- author = 'Michael Elsdoerfer',
- author_email = 'michael@elsdoerfer.info',
- license = 'BSD',
- url = 'http://launchpad.net/django-tables',
+ version = __import__(package_dir).get_version().replace(' ', '-'),
+ description = 'Table framework for Django',
+ author = 'Bradley Ayers',
+ author_email = 'bradley.ayers@gmail.com',
+ url = '',
classifiers = [
- 'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Software Development :: Libraries',
- ],
- packages = find_packages('django_tables'),
+ ],
+ packages = packages,
+ data_files = data_files,
+ cmdclass = cmdclasses,
+ requires = ['django(>=1.1)'],
+ install_requires = ['django>=1.1']
)
+++ /dev/null
-# make django-tables available for import for tests
-import os, sys
-sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+++ /dev/null
-"""Test the core table functionality.
-"""
-
-
-from nose.tools import assert_raises, assert_equal
-from django.http import Http404
-from django.core.paginator import Paginator
-import django_tables as tables
-from django_tables.base import BaseTable
-
-
-class TestTable(BaseTable):
- pass
-
-
-def test_declaration():
- """
- Test defining tables by declaration.
- """
-
- class GeoAreaTable(TestTable):
- name = tables.Column()
- population = tables.Column()
-
- assert len(GeoAreaTable.base_columns) == 2
- assert 'name' in GeoAreaTable.base_columns
- assert not hasattr(GeoAreaTable, 'name')
-
- class CountryTable(GeoAreaTable):
- capital = tables.Column()
-
- assert len(CountryTable.base_columns) == 3
- assert 'capital' in CountryTable.base_columns
-
- # multiple inheritance
- class AddedMixin(TestTable):
- added = tables.Column()
- class CityTable(GeoAreaTable, AddedMixin):
- mayer = tables.Column()
-
- assert len(CityTable.base_columns) == 4
- assert 'added' in CityTable.base_columns
-
- # modelforms: support switching from a non-model table hierarchy to a
- # modeltable hierarchy (both base class orders)
- class StateTable1(tables.ModelTable, GeoAreaTable):
- motto = tables.Column()
- class StateTable2(GeoAreaTable, tables.ModelTable):
- motto = tables.Column()
-
- assert len(StateTable1.base_columns) == len(StateTable2.base_columns) == 3
- assert 'motto' in StateTable1.base_columns
- assert 'motto' in StateTable2.base_columns
-
-
-def test_sort():
- class MyUnsortedTable(TestTable):
- alpha = tables.Column()
- beta = tables.Column()
- n = tables.Column()
-
- test_data = [
- {'alpha': "mmm", 'beta': "mmm", 'n': 1 },
- {'alpha': "aaa", 'beta': "zzz", 'n': 2 },
- {'alpha': "zzz", 'beta': "aaa", 'n': 3 }]
-
- # various different ways to say the same thing: don't sort
- assert_equal(MyUnsortedTable(test_data ).order_by, ())
- assert_equal(MyUnsortedTable(test_data, order_by=None).order_by, ())
- assert_equal(MyUnsortedTable(test_data, order_by=[] ).order_by, ())
- assert_equal(MyUnsortedTable(test_data, order_by=() ).order_by, ())
-
- # values of order_by are wrapped in tuples before being returned
- assert_equal(('alpha',), MyUnsortedTable([], order_by='alpha').order_by)
- assert_equal(('beta',), MyUnsortedTable([], order_by=('beta',)).order_by)
- assert_equal((), MyUnsortedTable([]).order_by)
-
- # a rewritten order_by is also wrapped
- table = MyUnsortedTable([])
- table.order_by = 'alpha'
- assert_equal(('alpha',), table.order_by)
-
- # default sort order can be specified in table options
- class MySortedTable(MyUnsortedTable):
- class Meta:
- order_by = 'alpha'
-
- # order_by is inherited from the options if not explitly set
- table = MySortedTable(test_data)
- assert_equal(('alpha',), table.order_by)
-
- # ...but can be overloaded at __init___
- table = MySortedTable(test_data, order_by='beta')
- assert_equal(('beta',), table.order_by)
-
- # ...or rewritten later
- table = MySortedTable(test_data)
- table.order_by = 'beta'
- assert_equal(('beta',), table.order_by)
-
- # ...or reset to None (unsorted), ignoring the table default
- table = MySortedTable(test_data, order_by=None)
- assert_equal((), table.order_by)
- assert_equal(1, table.rows[0]['n'])
-
-
-def test_column_count():
- class MyTable(TestTable):
- visbible = tables.Column(visible=True)
- hidden = tables.Column(visible=False)
-
- # The columns container supports the len() builtin
- assert len(MyTable([]).columns) == 1
-
-
-def test_pagination():
- class BookTable(TestTable):
- name = tables.Column()
-
- # create some sample data
- data = []
- for i in range(1,101):
- data.append({'name': 'Book Nr. %d'%i})
- books = BookTable(data)
-
- # external paginator
- paginator = Paginator(books.rows, 10)
- assert paginator.num_pages == 10
- page = paginator.page(1)
- assert len(page.object_list) == 10
- assert page.has_previous() == False
- assert page.has_next() == True
-
- # integrated paginator
- books.paginate(Paginator, 10, page=1)
- # rows is now paginated
- assert len(list(books.rows.page())) == 10
- assert len(list(books.rows.all())) == 100
- # new attributes
- assert books.paginator.num_pages == 10
- assert books.page.has_previous() == False
- assert books.page.has_next() == True
- # exceptions are converted into 404s
- assert_raises(Http404, books.paginate, Paginator, 10, page=9999)
- assert_raises(Http404, books.paginate, Paginator, 10, page="abc")
+++ /dev/null
-# coding: utf8
-"""Test template specific functionality.
-
-Make sure tables expose their functionality to templates right. This
-generally about testing "out"-functionality of the tables, whether
-via templates or otherwise. Whether a test belongs here or, say, in
-``test_basic``, is not always a clear-cut decision.
-"""
-
-from django.template import Template, Context, add_to_builtins
-from django.http import HttpRequest
-import django_tables as tables
-
-def test_order_by():
- class BookTable(tables.MemoryTable):
- id = tables.Column()
- name = tables.Column()
- books = BookTable([
- {'id': 1, 'name': 'Foo: Bar'},
- ])
-
- # cast to a string we get a value ready to be passed to the querystring
- books.order_by = ('name',)
- assert str(books.order_by) == 'name'
- books.order_by = ('name', '-id')
- assert str(books.order_by) == 'name,-id'
-
-def test_columns_and_rows():
- class CountryTable(tables.MemoryTable):
- name = tables.TextColumn()
- capital = tables.TextColumn(sortable=False)
- 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.")
-
- countries = CountryTable(
- [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'cc': 49},
- {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'cc': 33},
- {'name': 'Netherlands', 'capital': 'Amsterdam', 'cc': '31'},
- {'name': 'Austria', 'cc': 43, 'currency': 'Euro (€)', 'population': 8}])
-
- assert len(list(countries.columns)) == 4
- assert len(list(countries.rows)) == len(list(countries)) == 4
-
- # column name override, hidden columns
- assert [c.name for c in countries.columns] == ['name', 'capital', 'population', 'cc']
- # verbose_name, and fallback to field name
- assert [unicode(c) for c in countries.columns] == ['Name', 'Capital', 'Population Size', 'Phone Ext.']
-
- # data yielded by each row matches the defined columns
- for row in countries.rows:
- assert len(list(row)) == len(list(countries.columns))
-
- # we can access each column and row by name...
- assert countries.columns['population'].column.verbose_name == "Population Size"
- assert countries.columns['cc'].column.verbose_name == "Phone Ext."
- # ...even invisible ones
- assert countries.columns['tld'].column.verbose_name == "Domain"
- # ...and even inaccessible ones (but accessible to the coder)
- assert countries.columns['currency'].column == countries.base_columns['currency']
- # this also works for rows
- for row in countries:
- row['tld'], row['cc'], row['population']
-
- # certain data is available on columns
- assert countries.columns['currency'].sortable == True
- assert countries.columns['capital'].sortable == False
- assert countries.columns['name'].visible == True
- assert countries.columns['tld'].visible == False
-
-def test_render():
- """For good measure, render some actual templates."""
-
- 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")
- calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")
-
- 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 49 France None 64 33 Netherlands Amsterdam None 31 Austria None 8 43 "
-
- print Template("{% for row in countries %}{% if countries.columns.name.visible %}{{ row.name }} {% endif %}{% if countries.columns.tld.visible %}{{ row.tld }} {% endif %}{% endfor %}").\
- render(Context({'countries': countries})) == \
- "Germany France Netherlands Austria"
-
-def test_templatetags():
- add_to_builtins('django_tables.app.templatetags.tables')
-
- # [bug] set url param tag handles an order_by tuple with multiple columns
- class MyTable(tables.MemoryTable):
- f1 = tables.Column()
- f2 = tables.Column()
- t = Template('{% set_url_param x=table.order_by %}')
- table = MyTable([], order_by=('f1', 'f2'))
- assert t.render(Context({'request': HttpRequest(), 'table': table})) == '?x=f1%2Cf2'