* Moved tests out of the app, and integrated them with setup.py v0.4.0.alpha2
authorBradley Ayers <bradley.ayers@enigmainteractive.com>
Thu, 24 Mar 2011 03:13:12 +0000 (13:13 +1000)
committerBradley Ayers <bradley.ayers@enigmainteractive.com>
Thu, 24 Mar 2011 03:13:12 +0000 (13:13 +1000)
* Added more tests for QuerySet data sources
* Clarified documentation
* Renamed BoundRow.data to BoundRow.record
* Fixed bug in conf.py, where it was trying to use the old VERSION variable in django_tables.
* Updated the documentation to clarify the use-cases for column formatters.
* Simplified setup.py
* Using QuerySets actually works now, added a relevant test that now uses django-attest

18 files changed:
.gitignore
MANIFEST.in [new file with mode: 0644]
django_tables/__init__.py
django_tables/rows.py
django_tables/tables.py
django_tables/templatetags/django_tables.py
django_tables/tests/__init__.py [deleted file]
django_tables/tests/memory.py [deleted file]
django_tables/tests/models.py [deleted file]
django_tables/tests/testapp/__init__.py [deleted file]
django_tables/tests/testapp/models.py [deleted file]
docs/conf.py
docs/index.rst
setup.py
tests/__init__.py [new file with mode: 0644]
tests/core.py [moved from django_tables/tests/core.py with 99% similarity]
tests/models.py [new file with mode: 0644]
tests/templates.py [moved from django_tables/tests/templates.py with 100% similarity]

index 53b958f3ca989d384ed5c537c74f44a4265b1591..6eabac012a9c3e2a01aa739b0a606d17e00dc21e 100644 (file)
@@ -1,7 +1,8 @@
 *.pyc
 /*.komodoproject
+/*.egg-info/
+/*.egg
 /MANIFEST
 /dist/
 /build/
 /docs/_build/
-/django_tables.egg-info/
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..de73873
--- /dev/null
@@ -0,0 +1,2 @@
+include README.rst
+recursive-include django_tables/templates *
index 38774d17960fcefcb591a5b8656783c7f25c9902..1b1fad541932273ebefd4f5f931d4eab3e0218a0 100644 (file)
@@ -1,43 +1,2 @@
-# -*- coding: utf8 -*-
-# (major, minor, bugfix, "pre-alpha" | "alpha" | "beta" | "final", release | 0)
-VERSION = (0, 4, 0, 'alpha', 1)
-
-
-def get_version():
-    version = '%s.%s' % (VERSION[0], VERSION[1])
-    if VERSION[2]:
-        version = '%s.%s' % (version, VERSION[2])
-    if VERSION[3:] == ('alpha', 0):
-        version = '%s pre-alpha' % version
-    else:
-        if VERSION[3] != 'final':
-            version = '%s %s %s' % (version, VERSION[3], VERSION[4])
-    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.
-try:
-    import django
-except ImportError:
-    import warnings
-    warnings.warn('django-tables requires Django, however it is not installed.'
-        ' Version information will still be available.')
-else:
-    try:
-        # http://docs.djangoproject.com/en/dev/topics/settings/ says::
-        #
-        #   If you don't set DJANGO_SETTINGS_MODULE and don't call configure(),
-        #   Django will raise an ImportError exception the first time a setting is
-        #   accessed.
-        #
-        from django.conf import settings
-        settings.DEBUG  # will raise ImportError if Django isn't configured
-    except ImportError:
-        # allow get_version() to remain available
-        import warnings
-        warnings.warn('django-tables requires Django to be configured... but '
-            "it isn't! A bunch of stuff won't work :(")
-
-    from tables import *
-    from columns import *
+from .tables import *
+from .columns import *
index 9544bd31f73e0675195305244dfb61fea54ceacc..a6f9ed9479f6fb8ef83eecd5795fc2613b28fe01 100644 (file)
@@ -54,17 +54,30 @@ class BoundRow(object):
         KeyError: 'c'
 
     """
-    def __init__(self, table, data):
+    def __init__(self, table, record):
         """Initialise a new :class:`BoundRow` object where:
 
         * *table* is the :class:`Table` in which this row exists.
-        * *data* is a chunk of data that describes the information for this
-          row. A "chunk" of data might be a :class:`Model` object, a ``dict``,
-          or perhaps something else.
+        * *record* is a single record from the data source that is posed to
+          populate the row. A record could be a :class:`Model` object, a
+          ``dict``, or something else.
 
         """
-        self.table = table
-        self.data = data
+        self._table = table
+        self._record = record
+
+    @property
+    def table(self):
+        """The associated :term:`table`."""
+        return self._table
+
+    @property
+    def record(self):
+        """The data record from the data source which is used to populate this
+        row with data.
+
+        """
+        return self._record
 
     def __iter__(self):
         """Iterate over the rendered values for cells in the row.
@@ -73,8 +86,10 @@ class BoundRow(object):
         each cell.
 
         """
-        for value in self.values:
-            yield value
+        for column in self.table.columns:
+            # this uses __getitem__, using the name (rather than the accessor)
+            # is correct – it's what __getitem__ expects.
+            yield self[column.name]
 
     def __getitem__(self, name):
         """Returns the final rendered value for a cell in the row, given the
@@ -96,13 +111,6 @@ class BoundRow(object):
         else:
             return item in self
 
-    @property
-    def values(self):
-        for column in self.table.columns:
-            # this uses __getitem__, using the name (rather than the accessor)
-            # is correct – it's what __getitem__ expects.
-            yield self[column.name]
-
 
 class Rows(object):
     """Container for spawning BoundRows.
index df26d8f747b623807f6396561b4d229c70df4c7a..2897e50132fc4f27614e2a59d0bed4e04d0c92f3 100644 (file)
@@ -16,38 +16,46 @@ __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.
+    """Exposes a consistent API for a table data. It currently supports a
+    :class:`QuerySet` or a ``list`` of ``dict``s.
+
     """
     def __init__(self, data, table):
         from django.db.models.query import QuerySet
-        self._data = data if not isinstance(data, QuerySet) else None
-        self._queryset = data if isinstance(data, QuerySet) else None
+        if isinstance(data, QuerySet):
+            self.queryset = data
+        elif isinstance(data, list):
+            self.list = data
+        else:
+            raise ValueError('data must be a list or QuerySet object, not %s'
+                             % data.__class__.__name__)
         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)
+        if hasattr(self, 'list'):
+            self.list = copy.copy(self.list)
+            self._populate_missing_values(self.list)
 
     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)
+        return (self.queryset.count() if hasattr(self, 'queryset')
+                                      else len(self.list))
 
     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:
+        if hasattr(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)
+            order_by = [o.replace(Accessor.SEPARATOR,
+                                  QUERYSET_ACCESSOR_SEPARATOR)
+                        for o in order_by]
+            self.queryset = self.queryset.order_by(*order_by)
         else:
-            self._data.sort(cmp=order_by.cmp)
+            self.list.sort(cmp=order_by.cmp)
 
     def _translate_order_by(self, order_by):
         """Translate from column names to column accessors"""
@@ -68,6 +76,7 @@ class TableData(object):
         """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
@@ -99,10 +108,11 @@ class TableData(object):
     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.
+        :param formatting:
+            Apply column formatter after retrieving the value from the data.
+
         """
-        value = Accessor(bound_column.accessor).resolve(bound_row.data)
+        value = Accessor(bound_column.accessor).resolve(bound_row.record)
         # 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()
@@ -110,34 +120,19 @@ class TableData(object):
             value = bound_column.formatter(value)
         return value
 
-    def __getitem__(self, key):
-        return self._data[key]
+    def __getitem__(self, index):
+        return (self.list if hasattr(self, 'list') else self.queryset)[index]
 
 
 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.
+    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.
+        """Ughhh document this :)
+
         """
         # extract declared columns
         columns = [(name, attrs.pop(name)) for name, column in attrs.items()
@@ -188,8 +183,13 @@ class Table(StrAndUnicode):
     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.
+        :param order_by:
+            If specified, it must be a sequence containing the names of columns
+            in the order that they should be ordered (much the same as
+            :method:`QuerySet.order_by`)
+
+            If not specified, the table will fall back to the
+            :attr:`Meta.order_by` setting.
 
         Note that unlike a ``Form``, tables are always bound to data. Also
         unlike a form, the ``columns`` attribute is read-only and returns
@@ -200,6 +200,7 @@ class Table(StrAndUnicode):
         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
index db15886367dce31f62d7c9591805b8f4729573eb..b914f4e4045592bb25aebf7de422cec50a723e40 100644 (file)
@@ -1,6 +1,6 @@
 """
-Allows setting/changing/removing of chosen url query string parameters,
-while maintaining any existing others.
+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``.
 
@@ -9,8 +9,8 @@ Examples:
     {% set_url_param page=next_page %}
     {% set_url_param page="" %}
     {% set_url_param filter="books" page=1 %}
-"""
 
+"""
 import urllib
 import tokenize
 import StringIO
@@ -18,6 +18,7 @@ from django import template
 from django.template.loader import get_template
 from django.utils.safestring import mark_safe
 
+
 register = template.Library()
 
 
@@ -59,7 +60,8 @@ class SetUrlParamNode(template.Node):
         return '?' + urllib.urlencode(params, doseq=True)
 
 
-def do_set_url_param(parser, token):
+@register.tag
+def set_url_param(parser, token):
     bits = token.contents.split()
     qschanges = {}
     for i in bits[1:]:
@@ -81,8 +83,6 @@ def do_set_url_param(parser, token):
                    "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):
@@ -96,12 +96,11 @@ class RenderTableNode(template.Node):
         return get_template('django_tables/table.html').render(context)
 
 
-def do_render_table(parser, token):
+@register.tag
+def 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
deleted file mode 100644 (file)
index 2cedec1..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-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/memory.py b/django_tables/tests/memory.py
deleted file mode 100644 (file)
index 20d52ac..0000000
+++ /dev/null
@@ -1,349 +0,0 @@
-"""Test the memory table functionality.
-
-TODO: A bunch of those tests probably fit better into test_basic, since
-they aren't really MemoryTable specific.
-"""
-
-from math import sqrt
-from attest import Tests
-from django.core.paginator import Paginator
-import django_tables as tables
-
-memory = Tests()
-
-@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'},
-    ])
-
-    # access without order_by works
-    stuff.data
-    stuff.rows
-
-    # make sure BoundColumnn.name always gives us the right thing, whether
-    # the column explicitely defines a name or not.
-    stuff.columns['count'].name == 'count'
-    stuff.columns['answer'].name == 'answer'
-
-    for r in stuff.rows:
-        # unknown fields are removed/not-accessible
-        assert 'name' in r
-        assert not 'id' in r
-        # missing data is available as default
-        assert 'answer' in r
-        assert r['answer'] == 42   # note: different from prev. line!
-
-        # all that still works when name overrides are used
-        assert not 'c' in r
-        assert 'count' in r
-        assert r['count'] == 1
-
-        # columns with data= option work fine
-        assert r['email'] == 'foo@bar.org'
-
-    # [bug] splicing the table gives us valid, working rows
-    assert list(stuff[0]) == list(stuff.rows[0])
-    assert stuff[0]['name'] == 'Foo Bar'
-
-    # changing an instance's base_columns does not change the class
-    assert id(stuff.base_columns) != id(StuffTable.base_columns)
-    stuff.base_columns['test'] = tables.Column()
-    assert not 'test' in StuffTable.base_columns
-
-    # optionally, exceptions can be raised when input is invalid
-    tables.options.IGNORE_INVALID_OPTIONS = False
-    try:
-        assert_raises(ValueError, setattr, stuff, 'order_by', '-name,made-up-column')
-        assert_raises(ValueError, setattr, stuff, 'order_by', ('made-up-column',))
-        # when a column name is overwritten, the original won't work anymore
-        assert_raises(ValueError, setattr, stuff, 'order_by', 'c')
-        # reset for future tests
-    finally:
-        tables.options.IGNORE_INVALID_OPTIONS = True
-
-
-class TestRender:
-    """Test use of the render_* methods.
-    """
-
-    def test(self):
-        class TestTable(tables.MemoryTable):
-            private_name = tables.Column(name='public_name')
-            def render_public_name(self, data):
-                # We are given the actual data dict and have direct access
-                # to additional values for which no field is defined.
-                return "%s:%s" % (data['private_name'], data['additional'])
-
-        table = TestTable([{'private_name': 'FOO', 'additional': 'BAR'}])
-        assert table.rows[0]['public_name'] == 'FOO:BAR'
-
-    def test_not_sorted(self):
-        """The render methods are not considered when sorting.
-        """
-        class TestTable(tables.MemoryTable):
-            foo = tables.Column()
-            def render_foo(self, data):
-                return -data['foo']  # try to cause a reverse sort
-        table = TestTable([{'foo': 1}, {'foo': 2}], order_by='asc')
-        # Result is properly sorted, and the render function has never been called
-        assert [r['foo'] for r in table.rows] == [-1, -2]
-
-
-def test_caches():
-    """Ensure the various caches are effective.
-    """
-
-    class BookTable(tables.MemoryTable):
-        name = tables.Column()
-        answer = tables.Column(default=42)
-    books = BookTable([
-        {'name': 'Foo: Bar'},
-    ])
-
-    assert id(list(books.columns)[0]) == id(list(books.columns)[0])
-    # TODO: row cache currently not used
-    #assert id(list(books.rows)[0]) == id(list(books.rows)[0])
-
-    # test that caches are reset after an update()
-    old_column_cache = id(list(books.columns)[0])
-    old_row_cache = id(list(books.rows)[0])
-    books.update()
-    assert id(list(books.columns)[0]) != old_column_cache
-    assert id(list(books.rows)[0]) != old_row_cache
-
-def test_meta_sortable():
-    """Specific tests for sortable table meta option."""
-
-    def mktable(default_sortable):
-        class BookTable(tables.MemoryTable):
-            id = tables.Column(sortable=True)
-            name = tables.Column(sortable=False)
-            author = tables.Column()
-            class Meta:
-                sortable = default_sortable
-        return BookTable([])
-
-    global_table = mktable(None)
-    for default_sortable, results in (
-        (None,      (True, False, True)),    # last bool is global default
-        (True,      (True, False, True)),    # last bool is table default
-        (False,     (True, False, False)),   # last bool is table default
-    ):
-        books = mktable(default_sortable)
-        assert [c.sortable for c in books.columns] == list(results)
-
-        # it also works if the meta option is manually changed after
-        # class and instance creation
-        global_table._meta.sortable = default_sortable
-        assert [c.sortable for c in global_table.columns] == list(results)
-
-
-def test_sort():
-    class BookTable(tables.MemoryTable):
-        id = tables.Column(direction='desc')
-        name = tables.Column()
-        pages = tables.Column(name='num_pages')  # test rewritten names
-        language = tables.Column(default='en')   # default affects sorting
-        rating = tables.Column(data='*')         # test data field option
-
-    books = BookTable([
-        {'id': 1, 'pages':  60, 'name': 'Z: The Book', '*': 5},    # language: en
-        {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book', '*': 2},
-        {'id': 3, 'pages':  80, 'language': 'de', 'name': 'A: The Book, Vol. 2', '*': 4},
-        {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'},   # rating (with data option) is missing
-    ])
-
-    # None is normalized to an empty order by tuple, ensuring iterability;
-    # it also supports all the cool methods that we offer for order_by.
-    # This is true for the default case...
-    assert books.order_by == ()
-    iter(books.order_by)
-    assert hasattr(books.order_by, 'toggle')
-    # ...as well as when explicitly set to None.
-    books.order_by = None
-    assert books.order_by == ()
-    iter(books.order_by)
-    assert hasattr(books.order_by, 'toggle')
-
-    # test various orderings
-    def test_order(order, result):
-        books.order_by = order
-        assert [b['id'] for b in books.rows] == result
-    test_order(('num_pages',), [1,3,2,4])
-    test_order(('-num_pages',), [4,2,3,1])
-    test_order(('name',), [2,4,3,1])
-    test_order(('language', 'num_pages'), [3,2,1,4])
-    # using a simple string (for convinience as well as querystring passing
-    test_order('-num_pages', [4,2,3,1])
-    test_order('language,num_pages', [3,2,1,4])
-    # if overwritten, the declared fieldname has no effect
-    test_order('pages,name', [2,4,3,1])   # == ('name',)
-    # sort by column with "data" option
-    test_order('rating', [4,2,3,1])
-
-    # test the column with a default ``direction`` set to descending
-    test_order('id', [4,3,2,1])
-    test_order('-id', [1,2,3,4])
-    # changing the direction afterwards is fine too
-    books.base_columns['id'].direction = 'asc'
-    test_order('id', [1,2,3,4])
-    test_order('-id', [4,3,2,1])
-    # a invalid direction string raises an exception
-    assert_raises(ValueError, setattr, books.base_columns['id'], 'direction', 'blub')
-
-    # [bug] test alternative order formats if passed to constructor
-    BookTable([], 'language,-num_pages')
-
-    # test invalid order instructions
-    books.order_by = 'xyz'
-    assert not books.order_by
-    books.base_columns['language'].sortable = False
-    books.order_by = 'language'
-    assert not books.order_by
-    test_order(('language', 'num_pages'), [1,3,2,4])  # as if: 'num_pages'
-
-    # [bug] order_by did not run through setter when passed to init
-    books = BookTable([], order_by='name')
-    assert books.order_by == ('name',)
-
-    # test table.order_by extensions
-    books.order_by = ''
-    assert books.order_by.polarize(False) == ()
-    assert books.order_by.polarize(True) == ()
-    assert books.order_by.toggle() == ()
-    assert books.order_by.polarize(False, ['id']) == ('id',)
-    assert books.order_by.polarize(True, ['id']) == ('-id',)
-    assert books.order_by.toggle(['id']) == ('id',)
-    books.order_by = 'id,-name'
-    assert books.order_by.polarize(False, ['name']) == ('id', 'name')
-    assert books.order_by.polarize(True, ['name']) == ('id', '-name')
-    assert books.order_by.toggle(['name']) == ('id', 'name')
-    # ``in`` operator works
-    books.order_by = 'name'
-    assert 'name' in books.order_by
-    books.order_by = '-name'
-    assert 'name' in books.order_by
-    assert not 'language' in books.order_by
-
-
-def test_callable():
-    """Data fields and the ``default`` option can be callables.
-    """
-
-    class MathTable(tables.MemoryTable):
-        lhs = tables.Column()
-        rhs = tables.Column()
-        op = tables.Column(default='+')
-        sum = tables.Column(default=lambda d: calc(d['op'], d['lhs'], d['rhs']))
-
-    math = MathTable([
-        {'lhs': 1, 'rhs': lambda x: x['lhs']*3},              # 1+3
-        {'lhs': 9, 'rhs': lambda x: x['lhs'], 'op': '/'},     # 9/9
-        {'lhs': lambda x: x['rhs']+3, 'rhs': 4, 'op': '-'},   # 7-4
-    ])
-
-    # function is called when queried
-    def calc(op, lhs, rhs):
-        if op == '+': return lhs+rhs
-        elif op == '/': return lhs/rhs
-        elif op == '-': return lhs-rhs
-    assert [calc(row['op'], row['lhs'], row['rhs']) for row in math] == [4,1,3]
-
-    # field function is called while sorting
-    math.order_by = ('-rhs',)
-    assert [row['rhs'] for row in math] == [9,4,3]
-
-    # default function is called while sorting
-    math.order_by = ('sum',)
-    assert [row['sum'] for row in math] == [1,3,4]
-
-
-# TODO: all the column stuff might warrant it's own test file
-def test_columns():
-    """Test Table.columns container functionality.
-    """
-
-    class BookTable(tables.MemoryTable):
-        id = tables.Column(sortable=False, visible=False)
-        name = tables.Column(sortable=True)
-        pages = tables.Column(sortable=True)
-        language = tables.Column(sortable=False)
-    books = BookTable([])
-
-    assert list(books.columns.sortable()) == [c for c in books.columns if c.sortable]
-
-    # .columns iterator only yields visible columns
-    assert len(list(books.columns)) == 3
-    # visiblity of columns can be changed at instance-time
-    books.columns['id'].visible = True
-    assert len(list(books.columns)) == 4
-
-
-def test_column_order():
-    """Test the order functionality of bound columns.
-    """
-
-    class BookTable(tables.MemoryTable):
-        id = tables.Column()
-        name = tables.Column()
-        pages = tables.Column()
-        language = tables.Column()
-    books = BookTable([])
-
-    # the basic name property is a no-brainer
-    books.order_by = ''
-    assert [c.name for c in books.columns] == ['id','name','pages','language']
-
-    # name_reversed will always reverse, no matter what
-    for test in ['', 'name', '-name']:
-        books.order_by = test
-        assert [c.name_reversed for c in books.columns] == ['-id','-name','-pages','-language']
-
-    # name_toggled will always toggle
-    books.order_by = ''
-    assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']
-    books.order_by = 'id'
-    assert [c.name_toggled for c in books.columns] == ['-id','name','pages','language']
-    books.order_by = '-name'
-    assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']
-    # other columns in an order_by will be dismissed
-    books.order_by = '-id,name'
-    assert [c.name_toggled for c in books.columns] == ['id','-name','pages','language']
-
-    # with multi-column order, this is slightly more complex
-    books.order_by =  ''
-    assert [str(c.order_by) for c in books.columns] == ['id','name','pages','language']
-    assert [str(c.order_by_reversed) for c in books.columns] == ['-id','-name','-pages','-language']
-    assert [str(c.order_by_toggled) for c in books.columns] == ['id','name','pages','language']
-    books.order_by =  'id'
-    assert [str(c.order_by) for c in books.columns] == ['id','id,name','id,pages','id,language']
-    assert [str(c.order_by_reversed) for c in books.columns] == ['-id','id,-name','id,-pages','id,-language']
-    assert [str(c.order_by_toggled) for c in books.columns] == ['-id','id,name','id,pages','id,language']
-    books.order_by =  '-pages,id'
-    assert [str(c.order_by) for c in books.columns] == ['-pages,id','-pages,id,name','pages,id','-pages,id,language']
-    assert [str(c.order_by_reversed) for c in books.columns] == ['-pages,-id','-pages,id,-name','-pages,id','-pages,id,-language']
-    assert [str(c.order_by_toggled) for c in books.columns] == ['-pages,-id','-pages,id,name','pages,id','-pages,id,language']
-
-    # querying whether a column is ordered is possible
-    books.order_by = ''
-    assert [c.is_ordered for c in books.columns] == [False, False, False, False]
-    books.order_by = 'name'
-    assert [c.is_ordered for c in books.columns] == [False, True, False, False]
-    assert [c.is_ordered_reverse for c in books.columns] == [False, False, False, False]
-    assert [c.is_ordered_straight for c in books.columns] == [False, True, False, False]
-    books.order_by = '-pages'
-    assert [c.is_ordered for c in books.columns] == [False, False, True, False]
-    assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]
-    assert [c.is_ordered_straight for c in books.columns] == [False, False, False, False]
-    # and even works with multi-column ordering
-    books.order_by = 'id,-pages'
-    assert [c.is_ordered for c in books.columns] == [True, False, True, False]
-    assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]
-    assert [c.is_ordered_straight for c in books.columns] == [True, False, False, False]
diff --git a/django_tables/tests/models.py b/django_tables/tests/models.py
deleted file mode 100644 (file)
index c0bfaa5..0000000
+++ /dev/null
@@ -1,368 +0,0 @@
-"""Test ModelTable specific functionality.
-
-Sets up a temporary Django project using a memory SQLite database.
-"""
-
-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',
-        'DATABASE_NAME': ':memory:',
-        'INSTALLED_APPS': ('tests.testapp',)
-    })
-
-    from django.db import models
-    from django.core.management import call_command
-
-    class City(models.Model):
-        name = models.TextField()
-        population = models.IntegerField(null=True)
-        class Meta:
-            app_label = 'testapp'
-    module.City = City
-
-    class Country(models.Model):
-        name = models.TextField()
-        population = models.IntegerField()
-        capital = models.ForeignKey(City, blank=True, null=True)
-        tld = models.TextField(verbose_name='Domain Extension', max_length=2)
-        system = models.TextField(blank=True, null=True)
-        null = models.TextField(blank=True, null=True)   # tests expect this to be always null!
-        null2 = models.TextField(blank=True, null=True)  #  - " -
-        def example_domain(self):
-            return 'example.%s' % self.tld
-        class Meta:
-            app_label = 'testapp'
-    module.Country = Country
-
-    # create the tables
-    call_command('syncdb', verbosity=1, interactive=False)
-
-    # create a couple of objects
-    berlin=City(name="Berlin"); berlin.save()
-    amsterdam=City(name="Amsterdam"); amsterdam.save()
-    Country(name="Austria", tld="au", population=8, system="republic").save()
-    Country(name="Germany", tld="de", population=81, capital=berlin).save()
-    Country(name="France", tld="fr", population=64, system="republic").save()
-    Country(name="Netherlands", tld="nl", population=16, system="monarchy", capital=amsterdam).save()
-
-
-class TestDeclaration:
-    """Test declaration, declared columns and default model field columns.
-    """
-
-    def test_autogen_basic(self):
-        class CountryTable(tables.ModelTable):
-            class Meta:
-                model = Country
-
-        assert len(CountryTable.base_columns) == 8
-        assert 'name' in CountryTable.base_columns
-        assert not hasattr(CountryTable, 'name')
-
-        # Override one model column, add another custom one, exclude one
-        class CountryTable(tables.ModelTable):
-            capital = tables.TextColumn(verbose_name='Name of capital')
-            projected = tables.Column(verbose_name="Projected Population")
-            class Meta:
-                model = Country
-                exclude = ['tld']
-
-        assert len(CountryTable.base_columns) == 8
-        assert 'projected' in CountryTable.base_columns
-        assert 'capital' in CountryTable.base_columns
-        assert not 'tld' in CountryTable.base_columns
-
-        # Inheritance (with a different model) + field restrictions
-        class CityTable(CountryTable):
-            class Meta:
-                model = City
-                columns = ['id', 'name']
-                exclude = ['capital']
-
-        print CityTable.base_columns
-        assert len(CityTable.base_columns) == 4
-        assert 'id' in CityTable.base_columns
-        assert 'name' in CityTable.base_columns
-        assert 'projected' in CityTable.base_columns # declared in parent
-        assert not 'population' in CityTable.base_columns  # not in Meta:columns
-        assert 'capital' in CityTable.base_columns  # in exclude, but only works on model fields (is that the right behaviour?)
-
-    def test_columns_custom_order(self):
-        """Using the columns meta option, you can also modify the ordering.
-        """
-        class CountryTable(tables.ModelTable):
-            foo = tables.Column()
-            class Meta:
-                model = Country
-                columns = ('system', 'population', 'foo', 'tld',)
-
-        assert [c.name for c in CountryTable().columns] == ['system', 'population', 'foo', 'tld']
-
-    def test_columns_verbose_name(self):
-        """Tests that the model field's verbose_name is used for the column
-        """
-        class CountryTable(tables.ModelTable):
-            class Meta:
-                model = Country
-                columns = ('tld',)
-
-        assert [c.column.verbose_name for c in CountryTable().columns] == ['Domain Extension']
-
-
-def test_basic():
-    """Some tests here are copied from ``test_basic.py`` but need to be
-    rerun with a ModelTable, as the implementation is different."""
-
-    class CountryTable(tables.ModelTable):
-        null = tables.Column(default="foo")
-        tld = tables.Column(name="domain")
-        class Meta:
-            model = Country
-            exclude = ('id',)
-    countries = CountryTable()
-
-    def test_country_table(table):
-        for r in table.rows:
-            # "normal" fields exist
-            assert 'name' in r
-            # unknown fields are removed/not accessible
-            assert not 'does-not-exist' in r
-            # ...so are excluded fields
-            assert not 'id' in r
-            # [bug] access to data that might be available, but does not
-            # have a corresponding column is denied.
-            assert_raises(Exception, "r['id']")
-            # missing data is available with default values
-            assert 'null' in r
-            assert r['null'] == "foo"   # note: different from prev. line!
-            # if everything else fails (no default), we get None back
-            assert r['null2'] is None
-
-            # all that still works when name overrides are used
-            assert not 'tld' in r
-            assert 'domain' in r
-            assert len(r['domain']) == 2   # valid country tld
-    test_country_table(countries)
-
-    # repeat the avove tests with a table that is not associated with a
-    # model, and all columns being created manually.
-    class CountryTable(tables.ModelTable):
-        name = tables.Column()
-        population = tables.Column()
-        capital = tables.Column()
-        system = tables.Column()
-        null = tables.Column(default="foo")
-        null2 = tables.Column()
-        tld = tables.Column(name="domain")
-    countries = CountryTable(Country)
-    test_country_table(countries)
-
-
-def test_invalid_accessor():
-    """Test that a column being backed by a non-existent model property
-    is handled correctly.
-
-    Regression-Test: There used to be a NameError here.
-    """
-    class CountryTable(tables.ModelTable):
-        name = tables.Column(data='something-i-made-up')
-    countries = CountryTable(Country)
-    assert_raises(ValueError, countries[0].__getitem__, 'name')
-
-
-def test_caches():
-    """Make sure the caches work for model tables as well (parts are
-    reimplemented).
-    """
-    class CountryTable(tables.ModelTable):
-        class Meta:
-            model = Country
-            exclude = ('id',)
-    countries = CountryTable()
-
-    assert id(list(countries.columns)[0]) == id(list(countries.columns)[0])
-    # TODO: row cache currently not used
-    #assert id(list(countries.rows)[0]) == id(list(countries.rows)[0])
-
-    # test that caches are reset after an update()
-    old_column_cache = id(list(countries.columns)[0])
-    old_row_cache = id(list(countries.rows)[0])
-    countries.update()
-    assert id(list(countries.columns)[0]) != old_column_cache
-    assert id(list(countries.rows)[0]) != old_row_cache
-
-def test_sort():
-    class CountryTable(tables.ModelTable):
-        tld = tables.Column(name="domain")
-        population = tables.Column()
-        system = tables.Column(default="republic")
-        custom1 = tables.Column()
-        custom2 = tables.Column(sortable=True)
-        class Meta:
-            model = Country
-    countries = CountryTable()
-
-    def test_order(order, result, table=countries):
-        table.order_by = order
-        assert [r['id'] for r in table.rows] == result
-
-    # test various orderings
-    test_order(('population',), [1,4,3,2])
-    test_order(('-population',), [2,3,4,1])
-    test_order(('name',), [1,3,2,4])
-    # test sorting with a "rewritten" column name
-    countries.order_by = 'domain,tld'      # "tld" would be invalid...
-    countries.order_by == ('domain',)      # ...and is therefore removed
-    test_order(('-domain',), [4,3,2,1])
-    # test multiple order instructions; note: one row is missing a "system"
-    # value, but has a default set; however, that has no effect on sorting.
-    test_order(('system', '-population'), [2,4,3,1])
-    # using a simple string (for convinience as well as querystring passing)
-    test_order('-population', [2,3,4,1])
-    test_order('system,-population', [2,4,3,1])
-
-    # test column with a default ``direction`` set to descending
-    class CityTable(tables.ModelTable):
-        name = tables.Column(direction='desc')
-        class Meta:
-            model = City
-    cities = CityTable()
-    test_order('name', [1,2], table=cities)   # Berlin to Amsterdam
-    test_order('-name', [2,1], table=cities)  # Amsterdam to Berlin
-
-    # test invalid order instructions...
-    countries.order_by = 'invalid_field,population'
-    assert countries.order_by == ('population',)
-    # ...in case of ModelTables, this primarily means that only
-    # model-based colunns are currently sortable at all.
-    countries.order_by = ('custom1', 'custom2')
-    assert countries.order_by == ()
-
-def test_default_sort():
-    class SortedCountryTable(tables.ModelTable):
-        class Meta:
-            model = Country
-            order_by = '-name'
-
-    # the order_by option is provided by TableOptions
-    assert_equal('-name', SortedCountryTable()._meta.order_by)
-
-    # the default order can be inherited from the table
-    assert_equal(('-name',), SortedCountryTable().order_by)
-    assert_equal(4, SortedCountryTable().rows[0]['id'])
-
-    # and explicitly set (or reset) via __init__
-    assert_equal(2, SortedCountryTable(order_by='system').rows[0]['id'])
-    assert_equal(1, SortedCountryTable(order_by=None).rows[0]['id'])
-
-def test_callable():
-    """Some of the callable code is reimplemented for modeltables, so
-    test some specifics again.
-    """
-
-    class CountryTable(tables.ModelTable):
-        null = tables.Column(default=lambda s: s['example_domain'])
-        example_domain = tables.Column()
-        class Meta:
-            model = Country
-    countries = CountryTable(Country)
-
-    # model method is called
-    assert [row['example_domain'] for row in countries] == \
-                    ['example.'+row['tld'] for row in countries]
-
-    # column default method is called
-    assert [row['example_domain'] for row in countries] == \
-                    [row['null'] for row in countries]
-
-
-def test_relationships():
-    """Test relationship spanning."""
-
-    class CountryTable(tables.ModelTable):
-        # add relationship spanning columns (using different approaches)
-        capital_name = tables.Column(data='capital__name')
-        capital__population = tables.Column(name="capital_population")
-        invalid = tables.Column(data="capital__invalid")
-        class Meta:
-            model = Country
-    countries = CountryTable(Country.objects.select_related('capital'))
-
-    # ordering and field access works
-    countries.order_by = 'capital_name'
-    assert [row['capital_name'] for row in countries.rows] == \
-        [None, None, 'Amsterdam', 'Berlin']
-
-    countries.order_by = 'capital_population'
-    assert [row['capital_population'] for row in countries.rows] == \
-        [None, None, None, None]
-
-    # ordering by a column with an invalid relationship fails silently
-    countries.order_by = 'invalid'
-    assert countries.order_by == ()
-
-
-def test_pagination():
-    """Pretty much the same as static table pagination, but make sure we
-    provide the capability, at least for paginators that use it, to not
-    have the complete queryset loaded (by use of a count() query).
-
-    Note: This test changes the available cities, make sure it is last,
-    or that tests that follow are written appropriately.
-    """
-    from django.db import connection
-
-    class CityTable(tables.ModelTable):
-        class Meta:
-            model = City
-            columns = ['name']
-    cities = CityTable()
-
-    # add some sample data
-    City.objects.all().delete()
-    for i in range(1,101):
-        City.objects.create(name="City %d"%i)
-
-    # for query logging
-    settings.DEBUG = True
-
-    # external paginator
-    start_querycount = len(connection.queries)
-    paginator = Paginator(cities.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
-    # Make sure the queryset is not loaded completely - there must be two
-    # queries, one a count(). This check is far from foolproof...
-    assert len(connection.queries)-start_querycount == 2
-
-    # using a queryset paginator is possible as well (although unnecessary)
-    paginator = QuerySetPaginator(cities.rows, 10)
-    assert paginator.num_pages == 10
-
-    # integrated paginator
-    start_querycount = len(connection.queries)
-    cities.paginate(Paginator, 10, page=1)
-    # rows is now paginated
-    assert len(list(cities.rows.page())) == 10
-    assert len(list(cities.rows.all())) == 100
-    # new attributes
-    assert cities.paginator.num_pages == 10
-    assert cities.page.has_previous() == False
-    assert cities.page.has_next() == True
-    assert len(connection.queries)-start_querycount == 2
-
-    # reset
-    settings.DEBUG = False
-'''
diff --git a/django_tables/tests/testapp/__init__.py b/django_tables/tests/testapp/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/django_tables/tests/testapp/models.py b/django_tables/tests/testapp/models.py
deleted file mode 100644 (file)
index 7bedc38..0000000
+++ /dev/null
@@ -1 +0,0 @@
-"""Empty demo app our tests can assign models to."""
index 9fcd66ff7f140388a25007acb3c8aeb04b38151e..93ad7644b2ba3d0b5cfa5b7a2983dd0e7f1ef80b 100644 (file)
@@ -50,9 +50,9 @@ project = u'django-tables'
 # built documents.
 #
 # The short X.Y version.
-version = '.'.join(map(str, tables.VERSION[0:2]))
+version = '0.4.0.alpha2'
 # The full version, including alpha/beta/rc tags.
-release = tables.get_version()
+release = '0.4.0.alpha2'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
index 40cdf4a9b69a467749a961185d69c4af6807c99f..7cbe39649e08e42df5716e46fa4de2dde5e5d8db 100644 (file)
@@ -142,9 +142,12 @@ control you want, the less easy it is to use).
 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`:
+Using a formatter is a quick way to adjust the way values are displayed in a
+column. A limitation of this approach is that you *only* have access to a
+single attribute of the data source.
+
+To use a formatter, simply provide the :attr:`~Column.formatter` argument to a
+:class:`Column` when you define the :class:`Table`:
 
 .. code-block:: python
 
@@ -161,11 +164,18 @@ define the :class:`Table`:
     #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.
+As you can see, the only the value of the column is available to the formatter.
+This means that **it's impossible create a formatter that incorporates other
+values of the record**, e.g. a column with an ``<a href="...">`` that uses
+:func:`reverse` with the record's ``pk``.
+
+If formatters aren't powerful enough, you'll need to either :ref:`create a
+Column subclass <subclassing-column>`, or to use the
+:ref:`Table.render_FOO method <table.render_foo>`.
 
 
+.. _table.render_foo:
+
 :meth:`Table.render_FOO` Method
 -------------------------------
 
@@ -177,8 +187,10 @@ DRY).
 The example below has a number of different techniques in use:
 
 * :meth:`Column.render` (accessible via :attr:`BoundColumn.column`) applies the
-  *formatter* function if it's been provided. This is evident in the order that
-  the square and angled brackets have been applied for the ``id`` column.
+  *formatter* if it's been provided. The effect of this behaviour can be seen
+  below in the output for the ``id`` column. Square brackets (from the
+  *formatter*) have been applied *after* the angled brackets (from the
+  :meth:`~Table.render_FOO`).
 * Completely abitrary values can be returned by :meth:`render_FOO` methods, as
   shown in :meth:`~SimpleTable.render_row_number` (a :attr:`_counter` attribute
   is added to the :class:`SimpleTable` object to keep track of the row number).
@@ -213,6 +225,11 @@ The example below has a number of different techniques in use:
     <[10]>
     31 years old
 
+The :meth:`Column.render` method is what actually performs the lookup into a
+record to retrieve the column value. In the example above, the
+:meth:`render_row_number` never called :meth:`Column.render` and as a result
+there was not attempt to access the data source to retrieve a value.
+
 
 Custom Template
 ---------------
@@ -244,6 +261,8 @@ ignore the built-in generation tools, and instead pass an instance of your
     </table>
 
 
+.. _subclassing-column:
+
 Subclassing :class:`Column`
 ---------------------------
 
@@ -380,9 +399,15 @@ which can be iterated over:
 API Reference
 =============
 
-:class:`Column` Objects:
+:class:`Table` Objects:
 ------------------------
 
+.. autoclass:: django_tables.tables.Table
+    :members: __init__, data, order_by, rows, columns, as_html, paginate
+
+
+:class:`Column` Objects:
+------------------------
 
 .. autoclass:: django_tables.columns.Column
     :members: __init__, default, render
@@ -415,4 +440,14 @@ API Reference
 -------------------------
 
 .. autoclass:: django_tables.rows.BoundRow
-    :members: __init__, values, __getitem__, __contains__, __iter__
+    :members: __init__, __getitem__, __contains__, __iter__, record, table
+
+
+Glossary
+========
+
+.. glossary::
+
+    table
+        The traditional concept of a table. i.e. a grid of rows and columns
+        containing data.
index ba5d8f7dc597ba64a32eb1d0d75e4d6279cef19c..3b2c8cf0e5c33fe37fd5cc2abab5a4051b4b5ffa 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -1,82 +1,27 @@
-# -*- coding: utf8 -*-
-try:
-    from setuptools import setup
-except ImportError:
-    from distutils.core import setup
-from distutils.command.install_data import install_data
-from distutils.command.install import INSTALL_SCHEMES
-import os
-import sys
+#!/usr/bin/env python
+from setuptools import setup, find_packages
 
-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)
-
-if sys.platform == "darwin":
-    cmdclasses = {'install_data': osx_install_data}
-else:
-    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)
+setup(
+    name='django-tables',
+    version='0.4.0.alpha2',
+    description='Table framework for Django',
 
-# 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']
+    author='Bradley Ayers',
+    author_email='bradley.ayers@gmail.com',
+    license='Simplified BSD',
+    url='https://github.com/bradleyayers/django-tables/',
 
-# 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'
+    packages=find_packages(),
+    include_package_data=True,  # declarations in MANIFEST.in
 
-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]])
+    install_requires=['Django >=1.1'],
+    tests_require=['Django >=1.1', 'Attest >=0.4', 'django-attest'],
 
-# 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]
+    test_loader='attest:FancyReporter.test_loader',
+    test_suite='tests.everything',
 
-setup(
-    name = '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 = [
+    classifiers=[
         'Environment :: Web Environment',
         'Framework :: Django',
         'Intended Audience :: Developers',
@@ -86,9 +31,4 @@ setup(
         'Topic :: Internet :: WWW/HTTP',
         'Topic :: Software Development :: Libraries',
     ],
-    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
new file mode 100644 (file)
index 0000000..602f2c4
--- /dev/null
@@ -0,0 +1,28 @@
+from attest import AssertImportHook, Tests
+
+# Django's django.utils.module_loading.module_has_submodule is busted
+AssertImportHook.disable()
+
+
+from django.conf import settings
+
+# It's important to configure prior to importing the tests, as some of them
+# import Django's DB stuff.
+settings.configure(
+    DATABASES = {
+        'default': {
+            'ENGINE': 'django.db.backends.sqlite3',
+        }
+    },
+    INSTALLED_APPS = [
+        'django_tables'
+    ]
+)
+
+
+from .core import core
+from .templates import templates
+from .models import models
+
+
+everything = Tests([core, templates, models])
similarity index 99%
rename from django_tables/tests/core.py
rename to tests/core.py
index f22d7b2fcee8927e2867f0704bbed22f684a1f6d..93fbd2da9a360154403dbe0b71065b575764a2aa 100644 (file)
@@ -1,5 +1,4 @@
 """Test the core table functionality."""
-
 import copy
 from attest import Tests, Assert
 from django.http import Http404
diff --git a/tests/models.py b/tests/models.py
new file mode 100644 (file)
index 0000000..7a4c9d4
--- /dev/null
@@ -0,0 +1,45 @@
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.core.paginator import *
+import django_tables as tables
+from django_attest import TestContext
+from attest import Tests
+
+
+models = Tests()
+models.context(TestContext())
+
+
+@models.context
+def samples():
+    class Context(object):
+        class UserTable(tables.Table):
+            username = tables.Column()
+            first_name = tables.Column()
+            last_name = tables.Column()
+            email = tables.Column()
+            password = tables.Column()
+            is_staff = tables.Column()
+            is_active = tables.Column()
+            is_superuser = tables.Column()
+            last_login = tables.Column()
+            date_joined = tables.Column()
+
+    # we're going to test against User, so let's create a few
+    User.objects.create_user('fake-user-1', 'fake-1@example.com', 'password')
+    User.objects.create_user('fake-user-2', 'fake-2@example.com', 'password')
+    User.objects.create_user('fake-user-3', 'fake-3@example.com', 'password')
+    User.objects.create_user('fake-user-4', 'fake-4@example.com', 'password')
+
+    yield Context
+
+
+@models.test
+def simple(dj, samples):
+    users = User.objects.all()
+    table = samples.UserTable(users)
+
+    for index, row in enumerate(table.rows):
+        user = users[index]
+        Assert(user.username) == row['username']
+        Assert(user.email) == row['email']