lots of major changes
authorBradley Ayers <bradley.ayers@gmail.com>
Fri, 4 Feb 2011 02:28:44 +0000 (12:28 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Fri, 4 Feb 2011 02:28:44 +0000 (12:28 +1000)
48 files changed:
.gitignore
LICENSE [deleted file]
MANIFEST.in [deleted file]
README [deleted file]
TODO [deleted file]
django-tables.komodoproject [new file with mode: 0644]
django_tables/__init__.py
django_tables/app/models.py [deleted file]
django_tables/app/templatetags/tables.py [deleted file]
django_tables/base.py [deleted file]
django_tables/columns.py
django_tables/memory.py [deleted file]
django_tables/models.py
django_tables/options.py [deleted file]
django_tables/rows.py [new file with mode: 0644]
django_tables/tables.py [new file with mode: 0644]
django_tables/templates/django_tables/basic_table.html [new file with mode: 0644]
django_tables/templates/django_tables/table.html [new file with mode: 0644]
django_tables/templatetags/__init__.py [moved from django_tables/app/__init__.py with 100% similarity]
django_tables/templatetags/django_tables.py [new file with mode: 0644]
django_tables/tests/__init__.py [new file with mode: 0644]
django_tables/tests/core.py [new file with mode: 0644]
django_tables/tests/memory.py [moved from tests/test_memory.py with 98% similarity]
django_tables/tests/models.py [moved from tests/test_models.py with 99% similarity]
django_tables/tests/templates.py [new file with mode: 0644]
django_tables/tests/testapp/__init__.py [moved from django_tables/app/templatetags/__init__.py with 100% similarity]
django_tables/tests/testapp/models.py [moved from tests/testapp/models.py with 100% similarity]
django_tables/utils.py [new file with mode: 0644]
docs/Makefile
docs/columns.rst [deleted file]
docs/conf.py
docs/features/index.rst [deleted file]
docs/features/ordering.rst [deleted file]
docs/features/pagination.rst [deleted file]
docs/index.rst
docs/installation.rst [deleted file]
docs/make.bat
docs/templates.rst [deleted file]
docs/types/index.rst [deleted file]
docs/types/memory.rst [deleted file]
docs/types/models.rst [deleted file]
docs/types/sql.rst [deleted file]
requirements-dev.pip [deleted file]
setup.py
tests/__init__.py [deleted file]
tests/test_basic.py [deleted file]
tests/test_templates.py [deleted file]
tests/testapp/__init__.py [deleted file]

index 3bc50402c83945b31e8e7268659980d444d91145..64390b90e10dc4ac866d802151da6dde949a79c9 100644 (file)
@@ -1,13 +1,5 @@
 *.pyc
-
 /MANIFEST
-/dist
-/docs/_build/*
-/build
-
-/BRANCH_TODO
-
-# Project files
-/.project
-/.pydevproject
-/*.wpr
+dist/
+docs/_build/
+django_tables.egg-info/
diff --git a/LICENSE b/LICENSE
deleted file mode 100644 (file)
index a1eb278..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,27 +0,0 @@
-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.
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644 (file)
index 208f9d5..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-include README
-include LICENSE
-include MANIFEST.in
-recursive-include tests *.py
diff --git a/README b/README
deleted file mode 100644 (file)
index 2c28c87..0000000
--- a/README
+++ /dev/null
@@ -1,4 +0,0 @@
-django-tables - a Django QuerySet renderer.
-
-Documentation:
-    http://elsdoerfer.name/docs/django-tables/
diff --git a/TODO b/TODO
deleted file mode 100644 (file)
index 159c80c..0000000
--- a/TODO
+++ /dev/null
@@ -1,120 +0,0 @@
-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.
diff --git a/django-tables.komodoproject b/django-tables.komodoproject
new file mode 100644 (file)
index 0000000..bf27c2a
--- /dev/null
@@ -0,0 +1,10 @@
+<?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>
index 76d80698e6206387a5d462c6849383cf9d2fafa4..afe96dd54485744894074b0658f86285e2d35ab8 100644 (file)
@@ -1,7 +1,25 @@
-__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 *
diff --git a/django_tables/app/models.py b/django_tables/app/models.py
deleted file mode 100644 (file)
index 6a28b36..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-# Empty models.py file required for Django
-# INSTALLED_APPS loading.
diff --git a/django_tables/app/templatetags/tables.py b/django_tables/app/templatetags/tables.py
deleted file mode 100644 (file)
index f081ab4..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-# 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)
diff --git a/django_tables/base.py b/django_tables/base.py
deleted file mode 100644 (file)
index 75b5e95..0000000
+++ /dev/null
@@ -1,614 +0,0 @@
-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))
index 3307f09264f76026a444170fa8cbee2f0f7cc128..532f9b015988b90b5bb172c74200b29885ff102e 100644 (file)
@@ -1,33 +1,14 @@
-__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
@@ -38,48 +19,214 @@ class Column(object):
     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__)
diff --git a/django_tables/memory.py b/django_tables/memory.py
deleted file mode 100644 (file)
index 40d147c..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-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)
index b3ccb097a0e2e8d8fc546b1319e0c7ebb2e03bbf..1d38612417c53da7072c3c6c25849effac2b2bc3 100644 (file)
@@ -1,234 +1 @@
-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"""
diff --git a/django_tables/options.py b/django_tables/options.py
deleted file mode 100644 (file)
index 57331fb..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-"""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()
diff --git a/django_tables/rows.py b/django_tables/rows.py
new file mode 100644 (file)
index 0000000..9491023
--- /dev/null
@@ -0,0 +1,80 @@
+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.')
diff --git a/django_tables/tables.py b/django_tables/tables.py
new file mode 100644 (file)
index 0000000..db87cc2
--- /dev/null
@@ -0,0 +1,278 @@
+# -*- 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))
diff --git a/django_tables/templates/django_tables/basic_table.html b/django_tables/templates/django_tables/basic_table.html
new file mode 100644 (file)
index 0000000..50fb701
--- /dev/null
@@ -0,0 +1,20 @@
+{% 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 %}
diff --git a/django_tables/templates/django_tables/table.html b/django_tables/templates/django_tables/table.html
new file mode 100644 (file)
index 0000000..27ecfd6
--- /dev/null
@@ -0,0 +1,21 @@
+{% 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 %}
diff --git a/django_tables/templatetags/django_tables.py b/django_tables/templatetags/django_tables.py
new file mode 100644 (file)
index 0000000..db15886
--- /dev/null
@@ -0,0 +1,107 @@
+"""
+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)
diff --git a/django_tables/tests/__init__.py b/django_tables/tests/__init__.py
new file mode 100644 (file)
index 0000000..2cedec1
--- /dev/null
@@ -0,0 +1,13 @@
+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()
diff --git a/django_tables/tests/core.py b/django_tables/tests/core.py
new file mode 100644 (file)
index 0000000..f22d7b2
--- /dev/null
@@ -0,0 +1,206 @@
+"""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()
similarity index 98%
rename from tests/test_memory.py
rename to django_tables/tests/memory.py
index fe5ef6b3f8765d40dfd7888df59f01913b30ea1c..20d52acedec93863039477011076d62a76298f0f 100644 (file)
@@ -5,17 +5,20 @@ they aren't really MemoryTable specific.
 """
 
 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'},
     ])
@@ -45,10 +48,6 @@ def test_basic():
         # 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'
similarity index 99%
rename from tests/test_models.py
rename to django_tables/tests/models.py
index 0b3f79a3a58e652fa023eeb62ea6e454f5667fdd..c0bfaa5150e15fa2d2f178ceb6363a17c36a11f2 100644 (file)
@@ -3,12 +3,15 @@
 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',
@@ -362,3 +365,4 @@ def test_pagination():
 
     # reset
     settings.DEBUG = False
+'''
diff --git a/django_tables/tests/templates.py b/django_tables/tests/templates.py
new file mode 100644 (file)
index 0000000..a591ffa
--- /dev/null
@@ -0,0 +1,74 @@
+# -*- 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}))
diff --git a/django_tables/utils.py b/django_tables/utils.py
new file mode 100644 (file)
index 0000000..ac052b6
--- /dev/null
@@ -0,0 +1,143 @@
+# -*- 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)
index 05ce8182e9b15c2b5c6d27fa4a49b805d3b22a44..4dab0d95807f53e9d57880c7fca4694e107fb370 100644 (file)
@@ -12,20 +12,26 @@ PAPEROPT_a4     = -D latex_paper_size=a4
 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)/*
@@ -40,6 +46,11 @@ dirhtml:
        @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
@@ -65,12 +76,42 @@ qthelp:
        @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
diff --git a/docs/columns.rst b/docs/columns.rst
deleted file mode 100644 (file)
index e9c7a1a..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-=================
-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.
index 4a407dde811fa13bf3e1eaf4e32bbdde3f4c4df8..2463f2300a5da5719dcc97156898e35a56e2c27e 100644 (file)
@@ -1,7 +1,7 @@
 # -*- 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.
 #
@@ -16,13 +16,16 @@ import sys, os
 # 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']
@@ -31,23 +34,23 @@ 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.
@@ -59,12 +62,9 @@ release = '0.1'
 # 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
@@ -89,8 +89,8 @@ pygments_style = 'sphinx'
 
 # -- 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
@@ -138,7 +138,7 @@ html_static_path = ['_static']
 #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
@@ -149,13 +149,19 @@ html_static_path = ['_static']
 # 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'
@@ -172,8 +178,8 @@ 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
@@ -184,6 +190,12 @@ latex_documents = [
 # 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 = ''
 
@@ -191,4 +203,14 @@ latex_documents = [
 #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)
+]
diff --git a/docs/features/index.rst b/docs/features/index.rst
deleted file mode 100644 (file)
index e3e0a27..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-===============
-How to do stuff
-===============
-
-This section will explain some specific features in more detail.
-
-.. toctree::
-   :maxdepth: 1
-
-   ordering
-   pagination
diff --git a/docs/features/ordering.rst b/docs/features/ordering.rst
deleted file mode 100644 (file)
index 4f81f1d..0000000
+++ /dev/null
@@ -1,170 +0,0 @@
-=================
-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 %}
diff --git a/docs/features/pagination.rst b/docs/features/pagination.rst
deleted file mode 100644 (file)
index 7640ffe..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-----------
-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).
index a79fb6e8e60982d37ade9589306adbc5643538e5..1e3d091fd15212a656cd38a2b2a01bd864fe84b2 100644 (file)
-==========================================
-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.
diff --git a/docs/installation.rst b/docs/installation.rst
deleted file mode 100644 (file)
index 93bc128..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-------------
-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/
index 28bda72db08c9a679fd51bf80fa512931e0026f4..f2874c0c1681d025b92884db2e7f924251644ab3 100644 (file)
@@ -2,7 +2,9 @@
 
 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%" == "" (
@@ -14,16 +16,21 @@ if "%1" == "" goto help
 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
 )
 
@@ -35,6 +42,7 @@ if "%1" == "clean" (
 
 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
@@ -42,13 +50,23 @@ if "%1" == "html" (
 
 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
@@ -56,6 +74,7 @@ if "%1" == "pickle" (
 
 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
@@ -63,6 +82,7 @@ if "%1" == "json" (
 
 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.
@@ -71,6 +91,7 @@ if "%1" == "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:
@@ -80,15 +101,49 @@ if "%1" == "qthelp" (
        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
@@ -96,6 +151,7 @@ if "%1" == "changes" (
 
 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.
@@ -104,6 +160,7 @@ 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.
diff --git a/docs/templates.rst b/docs/templates.rst
deleted file mode 100644 (file)
index c3ee8bb..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-===================
-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).
diff --git a/docs/types/index.rst b/docs/types/index.rst
deleted file mode 100644 (file)
index 1aaac35..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-===========
-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>
diff --git a/docs/types/memory.rst b/docs/types/memory.rst
deleted file mode 100644 (file)
index ae09ae7..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
------------
-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).
diff --git a/docs/types/models.rst b/docs/types/models.rst
deleted file mode 100644 (file)
index 699a2f5..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-----------
-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.
diff --git a/docs/types/sql.rst b/docs/types/sql.rst
deleted file mode 100644 (file)
index 6f12f47..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
---------
-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.**
diff --git a/requirements-dev.pip b/requirements-dev.pip
deleted file mode 100644 (file)
index 8c5e259..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-django
-nose
-Sphinx
index 6e34acdc0bc8b4fbda5467f838c10d77c2ce3dbc..2dfeb428f02e0e1673aa4618f086c1b4f8670e4c 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -1,47 +1,79 @@
-#!/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',
@@ -50,6 +82,10 @@ setup(
         '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']
 )
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644 (file)
index db2ee0d..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-# make django-tables available for import for tests
-import os, sys
-sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
diff --git a/tests/test_basic.py b/tests/test_basic.py
deleted file mode 100644 (file)
index fda93f3..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-"""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")
diff --git a/tests/test_templates.py b/tests/test_templates.py
deleted file mode 100644 (file)
index 50f4dcb..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-# 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'
diff --git a/tests/testapp/__init__.py b/tests/testapp/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000