A table class looks very much like a form:\r
\r
import django_tables as tables\r
- class CountryTable(tables.Table):\r
+ class CountryTable(tables.MemoryTable):\r
name = tables.Column(verbose_name="Country Name")\r
population = tables.Column(sortable=False, visible=False)\r
time_zone = tables.Column(name="tz", default="UTC+1")\r
Table-specific options are implemented using the same inner ``Meta`` class\r
concept as known from forms and models in Django:\r
\r
- class MyTable(tables.Table):\r
+ class MyTable(tables.MemoryTable):\r
class Meta:\r
sortable = True\r
\r
\r
There are no required arguments. The following is fine:\r
\r
- class MyTable(tables.Table):\r
+ class MyTable(tables.MemoryTable):\r
c = tables.Column()\r
\r
It will result in a column named "c" in the table. You can specify the\r
correct filter class; \r
\r
If not using django-filter, we wouldn't have different filter types; filters \r
-would just hold the data, and each column would know how to apply it.
\ No newline at end of file
+would just hold the data, and each column would know how to apply it.\r
-from tables import *\r
+from memory import *\r
from models import *\r
-from columns import *
\ No newline at end of file
+from columns import *\r
+from base import *
\ No newline at end of file
from django.utils.text import capfirst\r
from columns import Column\r
\r
-__all__ = ('BaseTable', 'Table', 'options')\r
\r
-def sort_table(data, order_by):\r
- """Sort a list of dicts according to the fieldnames in the\r
- ``order_by`` iterable. Prefix with hypen for reverse.\r
+__all__ = ('BaseTable', 'options')\r
\r
- Dict values can be callables.\r
- """\r
- def _cmp(x, y):\r
- for name, reverse in instructions:\r
- lhs, rhs = x.get(name), y.get(name)\r
- res = cmp((callable(lhs) and [lhs(x)] or [lhs])[0],\r
- (callable(rhs) and [rhs(y)] or [rhs])[0])\r
- if res != 0:\r
- return reverse and -res or res\r
- return 0\r
- instructions = []\r
- for o in order_by:\r
- if o.startswith('-'):\r
- instructions.append((o[1:], True,))\r
- else:\r
- instructions.append((o, False,))\r
- data.sort(cmp=_cmp)\r
\r
class TableOptions(object):\r
def __init__(self, options=None):\r
super(TableOptions, self).__init__()\r
self.sortable = getattr(options, 'sortable', None)\r
\r
+\r
class DeclarativeColumnsMetaclass(type):\r
"""\r
Metaclass that converts Column attributes to a dictionary called\r
IGNORE_INVALID_OPTIONS = True\r
options = DefaultOptions()\r
\r
+\r
class BaseTable(object):\r
+ """A collection of columns, plus their associated data rows.\r
+ """\r
+\r
+ __metaclass__ = DeclarativeColumnsMetaclass\r
+\r
def __init__(self, data, order_by=None):\r
"""Create a new table instance with the iterable ``data``.\r
\r
self.base_columns = copy.deepcopy(type(self).base_columns)\r
\r
def _build_snapshot(self):\r
- """Rebuilds the table whenever it's options change.\r
+ """Rebuild the table for the current set of options.\r
\r
Whenver the table options change, e.g. say a new sort order,\r
this method will be asked to regenerate the actual table from\r
the linked data source.\r
\r
- In the case of this base table implementation, a copy of the\r
- source data is created, and then modified appropriately.\r
-\r
- # TODO: currently this is called whenever data changes; it is\r
- # probably much better to do this on-demand instead, when the\r
- # data is *needed* for the first time.\r
+ Subclasses should override this.\r
"""\r
-\r
- # reset caches\r
- self._columns._reset()\r
- self._rows._reset()\r
-\r
- snapshot = copy.copy(self._data)\r
- for row in snapshot:\r
- # add data that is missing from the source. we do this now so\r
- # that the colunn ``default`` and ``data`` values can affect\r
- # sorting (even when callables are used)!\r
- # This is a design decision - the alternative would be to\r
- # resolve the values when they are accessed, and either do not\r
- # support sorting them at all, or run the callables during\r
- # sorting.\r
- for column in self.columns.all():\r
- name_in_source = column.declared_name\r
- if column.column.data:\r
- if callable(column.column.data):\r
- # if data is a callable, use it's return value\r
- row[name_in_source] = column.column.data(BoundRow(self, row))\r
- else:\r
- name_in_source = column.column.data\r
-\r
- # the following will be True if:\r
- # * the source does not provide that column or provides None\r
- # * the column did provide a data callable that returned None\r
- if row.get(name_in_source, None) is None:\r
- row[name_in_source] = column.get_default(BoundRow(self, row))\r
-\r
- if self.order_by:\r
- actual_order_by = self._resolve_sort_directions(self.order_by)\r
- sort_table(snapshot, self._cols_to_fields(actual_order_by))\r
- self._snapshot = snapshot\r
+ self._snapshot = copy.copy(self._data)\r
\r
def _get_data(self):\r
if self._snapshot is None:\r
raise Http404(str(e))\r
\r
\r
-class Table(BaseTable):\r
- "A collection of columns, plus their associated data rows."\r
- # This is a separate class from BaseTable in order to abstract the way\r
- # self.columns is specified.\r
- __metaclass__ = DeclarativeColumnsMetaclass\r
-\r
-\r
class Columns(object):\r
"""Container for spawning BoundColumns.\r
\r
--- /dev/null
+import copy
+from base import BaseTable, BoundRow
+
+
+__all__ = ('MemoryTable', 'Table',)
+
+
+def sort_table(data, order_by):
+ """Sort a list of dicts according to the fieldnames in the
+ ``order_by`` iterable. Prefix with hypen for reverse.
+
+ Dict values can be callables.
+ """
+ def _cmp(x, y):
+ for name, reverse in instructions:
+ lhs, rhs = x.get(name), y.get(name)
+ res = cmp((callable(lhs) and [lhs(x)] or [lhs])[0],
+ (callable(rhs) and [rhs(y)] or [rhs])[0])
+ if res != 0:
+ return reverse and -res or res
+ return 0
+ instructions = []
+ for o in order_by:
+ if o.startswith('-'):
+ instructions.append((o[1:], True,))
+ else:
+ instructions.append((o, False,))
+ data.sort(cmp=_cmp)
+
+
+class MemoryTable(BaseTable):
+
+ # This is a separate class from BaseTable in order to abstract the way
+ # self.columns is specified.
+
+ 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))
+ self._snapshot = snapshot
+
+
+class Table(MemoryTable):
+ def __new__(cls, *a, **kw):
+ from warnings import warn
+ warn('"Table" has been renamed to "MemoryTable". Please use the '+
+ 'new name.', DeprecationWarning)
+ return MemoryTable.__new__(cls)
from django.core.exceptions import FieldError\r
from django.utils.datastructures import SortedDict\r
-from tables import BaseTable, DeclarativeColumnsMetaclass, \\r
+from base import BaseTable, DeclarativeColumnsMetaclass, \\r
Column, BoundRow, Rows, TableOptions, rmprefix, toggleprefix\r
\r
-__all__ = ('BaseModelTable', 'ModelTable')\r
+\r
+__all__ = ('ModelTable',)\r
+\r
\r
class ModelTableOptions(TableOptions):\r
def __init__(self, options=None):\r
self.columns = getattr(options, 'columns', None)\r
self.exclude = getattr(options, 'exclude', None)\r
\r
+\r
def columns_for_model(model, columns=None, exclude=None):\r
"""\r
Returns a ``SortedDict`` containing form columns for the given model.\r
self.base_columns = columns\r
return self\r
\r
-class BaseModelTable(BaseTable):\r
+\r
+class ModelTable(BaseTable):\r
"""Table that is based on a model.\r
\r
Similar to ModelForm, a column will automatically be created for all\r
just don't any data at all, the model the table is based on will\r
provide it.\r
"""\r
+\r
+ __metaclass__ = ModelTableMetaclass\r
+\r
def __init__(self, data=None, *args, **kwargs):\r
if data == None:\r
if self._meta.model is None:\r
else:\r
self.queryset = data\r
\r
- super(BaseModelTable, self).__init__(self.queryset, *args, **kwargs)\r
+ super(ModelTable, self).__init__(self.queryset, *args, **kwargs)\r
self._rows = ModelRows(self)\r
\r
def _validate_column_name(self, name, purpose):\r
spanning relationships to be sorted."""\r
\r
# let the base class sort out the easy ones\r
- result = super(BaseModelTable, self)._validate_column_name(name, purpose)\r
+ result = super(ModelTable, self)._validate_column_name(name, purpose)\r
if not result:\r
return False\r
\r
continue\r
try:\r
# Let Django validate the lookup by asking it to build\r
- # the final query; the way to do this has changed in \r
+ # the final query; the way to do this has changed in\r
# Django 1.2, and we try to support both versions.\r
_temp = self.queryset.order_by(lookup).query\r
if hasattr(_temp, 'as_sql'):\r
for row in self.data:\r
yield BoundModelRow(self, row)\r
\r
-class ModelTable(BaseModelTable):\r
- __metaclass__ = ModelTableMetaclass\r
\r
class ModelRows(Rows):\r
def __init__(self, *args, **kwargs):\r
-"""Test the base table functionality.\r
-\r
-This includes the core, as well as static data, non-model tables.\r
-"""\r
-\r
-from math import sqrt\r
-from nose.tools import assert_raises\r
-from django.core.paginator import Paginator\r
-from django.http import Http404\r
-import django_tables as tables\r
-\r
-def test_declaration():\r
- """\r
- Test defining tables by declaration.\r
- """\r
-\r
- class GeoAreaTable(tables.Table):\r
- name = tables.Column()\r
- population = tables.Column()\r
-\r
- assert len(GeoAreaTable.base_columns) == 2\r
- assert 'name' in GeoAreaTable.base_columns\r
- assert not hasattr(GeoAreaTable, 'name')\r
-\r
- class CountryTable(GeoAreaTable):\r
- capital = tables.Column()\r
-\r
- assert len(CountryTable.base_columns) == 3\r
- assert 'capital' in CountryTable.base_columns\r
-\r
- # multiple inheritance\r
- class AddedMixin(tables.Table):\r
- added = tables.Column()\r
- class CityTable(GeoAreaTable, AddedMixin):\r
- mayer = tables.Column()\r
-\r
- assert len(CityTable.base_columns) == 4\r
- assert 'added' in CityTable.base_columns\r
-\r
- # modelforms: support switching from a non-model table hierarchy to a\r
- # modeltable hierarchy (both base class orders)\r
- class StateTable1(tables.ModelTable, GeoAreaTable):\r
- motto = tables.Column()\r
- class StateTable2(GeoAreaTable, tables.ModelTable):\r
- motto = tables.Column()\r
-\r
- assert len(StateTable1.base_columns) == len(StateTable2.base_columns) == 3\r
- assert 'motto' in StateTable1.base_columns\r
- assert 'motto' in StateTable2.base_columns\r
-\r
-def test_basic():\r
- class StuffTable(tables.Table):\r
- name = tables.Column()\r
- answer = tables.Column(default=42)\r
- c = tables.Column(name="count", default=1)\r
- email = tables.Column(data="@")\r
- stuff = StuffTable([\r
- {'id': 1, 'name': 'Foo Bar', '@': 'foo@bar.org'},\r
- ])\r
-\r
- # access without order_by works\r
- stuff.data\r
- stuff.rows\r
-\r
- # make sure BoundColumnn.name always gives us the right thing, whether\r
- # the column explicitely defines a name or not.\r
- stuff.columns['count'].name == 'count'\r
- stuff.columns['answer'].name == 'answer'\r
-\r
- for r in stuff.rows:\r
- # unknown fields are removed/not-accessible\r
- assert 'name' in r\r
- assert not 'id' in r\r
- # missing data is available as default\r
- assert 'answer' in r\r
- assert r['answer'] == 42 # note: different from prev. line!\r
-\r
- # all that still works when name overrides are used\r
- assert not 'c' in r\r
- assert 'count' in r\r
- assert r['count'] == 1\r
-\r
- # columns with data= option work fine\r
- assert r['email'] == 'foo@bar.org'\r
-\r
- # try to splice rows by index\r
- assert 'name' in stuff.rows[0]\r
- assert isinstance(stuff.rows[0:], list)\r
-\r
- # [bug] splicing the table gives us valid, working rows\r
- assert list(stuff[0]) == list(stuff.rows[0])\r
- assert stuff[0]['name'] == 'Foo Bar'\r
-\r
- # changing an instance's base_columns does not change the class\r
- assert id(stuff.base_columns) != id(StuffTable.base_columns)\r
- stuff.base_columns['test'] = tables.Column()\r
- assert not 'test' in StuffTable.base_columns\r
-\r
- # optionally, exceptions can be raised when input is invalid\r
- tables.options.IGNORE_INVALID_OPTIONS = False\r
- try:\r
- assert_raises(ValueError, setattr, stuff, 'order_by', '-name,made-up-column')\r
- assert_raises(ValueError, setattr, stuff, 'order_by', ('made-up-column',))\r
- # when a column name is overwritten, the original won't work anymore\r
- assert_raises(ValueError, setattr, stuff, 'order_by', 'c')\r
- # reset for future tests\r
- finally:\r
- tables.options.IGNORE_INVALID_OPTIONS = True\r
-\r
-def test_caches():\r
- """Ensure the various caches are effective.\r
- """\r
-\r
- class BookTable(tables.Table):\r
- name = tables.Column()\r
- answer = tables.Column(default=42)\r
- books = BookTable([\r
- {'name': 'Foo: Bar'},\r
- ])\r
-\r
- assert id(list(books.columns)[0]) == id(list(books.columns)[0])\r
- # TODO: row cache currently not used\r
- #assert id(list(books.rows)[0]) == id(list(books.rows)[0])\r
-\r
- # test that caches are reset after an update()\r
- old_column_cache = id(list(books.columns)[0])\r
- old_row_cache = id(list(books.rows)[0])\r
- books.update()\r
- assert id(list(books.columns)[0]) != old_column_cache\r
- assert id(list(books.rows)[0]) != old_row_cache\r
-\r
-def test_meta_sortable():\r
- """Specific tests for sortable table meta option."""\r
-\r
- def mktable(default_sortable):\r
- class BookTable(tables.Table):\r
- id = tables.Column(sortable=True)\r
- name = tables.Column(sortable=False)\r
- author = tables.Column()\r
- class Meta:\r
- sortable = default_sortable\r
- return BookTable([])\r
-\r
- global_table = mktable(None)\r
- for default_sortable, results in (\r
- (None, (True, False, True)), # last bool is global default\r
- (True, (True, False, True)), # last bool is table default\r
- (False, (True, False, False)), # last bool is table default\r
- ):\r
- books = mktable(default_sortable)\r
- assert [c.sortable for c in books.columns] == list(results)\r
-\r
- # it also works if the meta option is manually changed after\r
- # class and instance creation\r
- global_table._meta.sortable = default_sortable\r
- assert [c.sortable for c in global_table.columns] == list(results)\r
-\r
-def test_sort():\r
- class BookTable(tables.Table):\r
- id = tables.Column(direction='desc')\r
- name = tables.Column()\r
- pages = tables.Column(name='num_pages') # test rewritten names\r
- language = tables.Column(default='en') # default affects sorting\r
- rating = tables.Column(data='*') # test data field option\r
-\r
- books = BookTable([\r
- {'id': 1, 'pages': 60, 'name': 'Z: The Book', '*': 5}, # language: en\r
- {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book', '*': 2},\r
- {'id': 3, 'pages': 80, 'language': 'de', 'name': 'A: The Book, Vol. 2', '*': 4},\r
- {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'}, # rating (with data option) is missing\r
- ])\r
-\r
- # None is normalized to an empty order by tuple, ensuring iterability;\r
- # it also supports all the cool methods that we offer for order_by.\r
- # This is true for the default case...\r
- assert books.order_by == ()\r
- iter(books.order_by)\r
- assert hasattr(books.order_by, 'toggle')\r
- # ...as well as when explicitly set to None.\r
- books.order_by = None\r
- assert books.order_by == ()\r
- iter(books.order_by)\r
- assert hasattr(books.order_by, 'toggle')\r
-\r
- # test various orderings\r
- def test_order(order, result):\r
- books.order_by = order\r
- assert [b['id'] for b in books.rows] == result\r
- test_order(('num_pages',), [1,3,2,4])\r
- test_order(('-num_pages',), [4,2,3,1])\r
- test_order(('name',), [2,4,3,1])\r
- test_order(('language', 'num_pages'), [3,2,1,4])\r
- # using a simple string (for convinience as well as querystring passing\r
- test_order('-num_pages', [4,2,3,1])\r
- test_order('language,num_pages', [3,2,1,4])\r
- # if overwritten, the declared fieldname has no effect\r
- test_order('pages,name', [2,4,3,1]) # == ('name',)\r
- # sort by column with "data" option\r
- test_order('rating', [4,2,3,1])\r
-\r
- # test the column with a default ``direction`` set to descending\r
- test_order('id', [4,3,2,1])\r
- test_order('-id', [1,2,3,4])\r
- # changing the direction afterwards is fine too\r
- books.base_columns['id'].direction = 'asc'\r
- test_order('id', [1,2,3,4])\r
- test_order('-id', [4,3,2,1])\r
- # a invalid direction string raises an exception\r
- assert_raises(ValueError, setattr, books.base_columns['id'], 'direction', 'blub')\r
-\r
- # [bug] test alternative order formats if passed to constructor\r
- BookTable([], 'language,-num_pages')\r
-\r
- # test invalid order instructions\r
- books.order_by = 'xyz'\r
- assert not books.order_by\r
- books.base_columns['language'].sortable = False\r
- books.order_by = 'language'\r
- assert not books.order_by\r
- test_order(('language', 'num_pages'), [1,3,2,4]) # as if: 'num_pages'\r
-\r
- # [bug] order_by did not run through setter when passed to init\r
- books = BookTable([], order_by='name')\r
- assert books.order_by == ('name',)\r
-\r
- # test table.order_by extensions\r
- books.order_by = ''\r
- assert books.order_by.polarize(False) == ()\r
- assert books.order_by.polarize(True) == ()\r
- assert books.order_by.toggle() == ()\r
- assert books.order_by.polarize(False, ['id']) == ('id',)\r
- assert books.order_by.polarize(True, ['id']) == ('-id',)\r
- assert books.order_by.toggle(['id']) == ('id',)\r
- books.order_by = 'id,-name'\r
- assert books.order_by.polarize(False, ['name']) == ('id', 'name')\r
- assert books.order_by.polarize(True, ['name']) == ('id', '-name')\r
- assert books.order_by.toggle(['name']) == ('id', 'name')\r
- # ``in`` operator works\r
- books.order_by = 'name'\r
- assert 'name' in books.order_by\r
- books.order_by = '-name'\r
- assert 'name' in books.order_by\r
- assert not 'language' in books.order_by\r
-\r
-def test_callable():\r
- """Data fields, ``default`` and ``data`` options can be callables.\r
- """\r
-\r
- class MathTable(tables.Table):\r
- lhs = tables.Column()\r
- rhs = tables.Column()\r
- op = tables.Column(default='+')\r
- sum = tables.Column(default=lambda d: calc(d['op'], d['lhs'], d['rhs']))\r
- sqrt = tables.Column(data=lambda d: int(sqrt(d['sum'])))\r
-\r
- math = MathTable([\r
- {'lhs': 1, 'rhs': lambda x: x['lhs']*3}, # 1+3\r
- {'lhs': 9, 'rhs': lambda x: x['lhs'], 'op': '/'}, # 9/9\r
- {'lhs': lambda x: x['rhs']+3, 'rhs': 4, 'op': '-'}, # 7-4\r
- ])\r
-\r
- # function is called when queried\r
- def calc(op, lhs, rhs):\r
- if op == '+': return lhs+rhs\r
- elif op == '/': return lhs/rhs\r
- elif op == '-': return lhs-rhs\r
- assert [calc(row['op'], row['lhs'], row['rhs']) for row in math] == [4,1,3]\r
-\r
- # field function is called while sorting\r
- math.order_by = ('-rhs',)\r
- assert [row['rhs'] for row in math] == [9,4,3]\r
-\r
- # default function is called while sorting\r
- math.order_by = ('sum',)\r
- assert [row['sum'] for row in math] == [1,3,4]\r
-\r
- # data function is called while sorting\r
- math.order_by = ('sqrt',)\r
- assert [row['sqrt'] for row in math] == [1,1,2]\r
-\r
-def test_pagination():\r
- class BookTable(tables.Table):\r
- name = tables.Column()\r
-\r
- # create some sample data\r
- data = []\r
- for i in range(1,101):\r
- data.append({'name': 'Book Nr. %d'%i})\r
- books = BookTable(data)\r
-\r
- # external paginator\r
- paginator = Paginator(books.rows, 10)\r
- assert paginator.num_pages == 10\r
- page = paginator.page(1)\r
- assert len(page.object_list) == 10\r
- assert page.has_previous() == False\r
- assert page.has_next() == True\r
-\r
- # integrated paginator\r
- books.paginate(Paginator, 10, page=1)\r
- # rows is now paginated\r
- assert len(list(books.rows.page())) == 10\r
- assert len(list(books.rows.all())) == 100\r
- # new attributes\r
- assert books.paginator.num_pages == 10\r
- assert books.page.has_previous() == False\r
- assert books.page.has_next() == True\r
- # exceptions are converted into 404s\r
- assert_raises(Http404, books.paginate, Paginator, 10, page=9999)\r
- assert_raises(Http404, books.paginate, Paginator, 10, page="abc")\r
-\r
-\r
-# TODO: all the column stuff might warrant it's own test file\r
-def test_columns():\r
- """Test Table.columns container functionality.\r
- """\r
-\r
- class BookTable(tables.Table):\r
- id = tables.Column(sortable=False, visible=False)\r
- name = tables.Column(sortable=True)\r
- pages = tables.Column(sortable=True)\r
- language = tables.Column(sortable=False)\r
- books = BookTable([])\r
-\r
- assert list(books.columns.sortable()) == [c for c in books.columns if c.sortable]\r
-\r
- # .columns iterator only yields visible columns\r
- assert len(list(books.columns)) == 3\r
- # visiblity of columns can be changed at instance-time\r
- books.columns['id'].visible = True\r
- assert len(list(books.columns)) == 4\r
-\r
-\r
-def test_column_order():\r
- """Test the order functionality of bound columns.\r
- """\r
-\r
- class BookTable(tables.Table):\r
- id = tables.Column()\r
- name = tables.Column()\r
- pages = tables.Column()\r
- language = tables.Column()\r
- books = BookTable([])\r
-\r
- # the basic name property is a no-brainer\r
- books.order_by = ''\r
- assert [c.name for c in books.columns] == ['id','name','pages','language']\r
-\r
- # name_reversed will always reverse, no matter what\r
- for test in ['', 'name', '-name']:\r
- books.order_by = test\r
- assert [c.name_reversed for c in books.columns] == ['-id','-name','-pages','-language']\r
-\r
- # name_toggled will always toggle\r
- books.order_by = ''\r
- assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']\r
- books.order_by = 'id'\r
- assert [c.name_toggled for c in books.columns] == ['-id','name','pages','language']\r
- books.order_by = '-name'\r
- assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']\r
- # other columns in an order_by will be dismissed\r
- books.order_by = '-id,name'\r
- assert [c.name_toggled for c in books.columns] == ['id','-name','pages','language']\r
-\r
- # with multi-column order, this is slightly more complex\r
- books.order_by = ''\r
- assert [str(c.order_by) for c in books.columns] == ['id','name','pages','language']\r
- assert [str(c.order_by_reversed) for c in books.columns] == ['-id','-name','-pages','-language']\r
- assert [str(c.order_by_toggled) for c in books.columns] == ['id','name','pages','language']\r
- books.order_by = 'id'\r
- assert [str(c.order_by) for c in books.columns] == ['id','id,name','id,pages','id,language']\r
- assert [str(c.order_by_reversed) for c in books.columns] == ['-id','id,-name','id,-pages','id,-language']\r
- assert [str(c.order_by_toggled) for c in books.columns] == ['-id','id,name','id,pages','id,language']\r
- books.order_by = '-pages,id'\r
- assert [str(c.order_by) for c in books.columns] == ['-pages,id','-pages,id,name','pages,id','-pages,id,language']\r
- assert [str(c.order_by_reversed) for c in books.columns] == ['-pages,-id','-pages,id,-name','-pages,id','-pages,id,-language']\r
- assert [str(c.order_by_toggled) for c in books.columns] == ['-pages,-id','-pages,id,name','pages,id','-pages,id,language']\r
-\r
- # querying whether a column is ordered is possible\r
- books.order_by = ''\r
- assert [c.is_ordered for c in books.columns] == [False, False, False, False]\r
- books.order_by = 'name'\r
- assert [c.is_ordered for c in books.columns] == [False, True, False, False]\r
- assert [c.is_ordered_reverse for c in books.columns] == [False, False, False, False]\r
- assert [c.is_ordered_straight for c in books.columns] == [False, True, False, False]\r
- books.order_by = '-pages'\r
- assert [c.is_ordered for c in books.columns] == [False, False, True, False]\r
- assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]\r
- assert [c.is_ordered_straight for c in books.columns] == [False, False, False, False]\r
- # and even works with multi-column ordering\r
- books.order_by = 'id,-pages'\r
- assert [c.is_ordered for c in books.columns] == [True, False, True, False]\r
- assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]\r
- assert [c.is_ordered_straight for c in books.columns] == [True, False, False, False]
\ No newline at end of file
+"""Test the core table functionality.
+"""
+
+
+from nose.tools import assert_raises
+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_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")
\ No newline at end of file
--- /dev/null
+"""Test the memory table functionality.\r
+\r
+TODO: A bunch of those tests probably fit better into test_basic, since\r
+they aren't really MemoryTable specific.\r
+"""\r
+\r
+from math import sqrt\r
+from nose.tools import assert_raises\r
+from django.core.paginator import Paginator\r
+import django_tables as tables\r
+\r
+\r
+def test_basic():\r
+ class StuffTable(tables.MemoryTable):\r
+ name = tables.Column()\r
+ answer = tables.Column(default=42)\r
+ c = tables.Column(name="count", default=1)\r
+ email = tables.Column(data="@")\r
+ stuff = StuffTable([\r
+ {'id': 1, 'name': 'Foo Bar', '@': 'foo@bar.org'},\r
+ ])\r
+\r
+ # access without order_by works\r
+ stuff.data\r
+ stuff.rows\r
+\r
+ # make sure BoundColumnn.name always gives us the right thing, whether\r
+ # the column explicitely defines a name or not.\r
+ stuff.columns['count'].name == 'count'\r
+ stuff.columns['answer'].name == 'answer'\r
+\r
+ for r in stuff.rows:\r
+ # unknown fields are removed/not-accessible\r
+ assert 'name' in r\r
+ assert not 'id' in r\r
+ # missing data is available as default\r
+ assert 'answer' in r\r
+ assert r['answer'] == 42 # note: different from prev. line!\r
+\r
+ # all that still works when name overrides are used\r
+ assert not 'c' in r\r
+ assert 'count' in r\r
+ assert r['count'] == 1\r
+\r
+ # columns with data= option work fine\r
+ assert r['email'] == 'foo@bar.org'\r
+\r
+ # try to splice rows by index\r
+ assert 'name' in stuff.rows[0]\r
+ assert isinstance(stuff.rows[0:], list)\r
+\r
+ # [bug] splicing the table gives us valid, working rows\r
+ assert list(stuff[0]) == list(stuff.rows[0])\r
+ assert stuff[0]['name'] == 'Foo Bar'\r
+\r
+ # changing an instance's base_columns does not change the class\r
+ assert id(stuff.base_columns) != id(StuffTable.base_columns)\r
+ stuff.base_columns['test'] = tables.Column()\r
+ assert not 'test' in StuffTable.base_columns\r
+\r
+ # optionally, exceptions can be raised when input is invalid\r
+ tables.options.IGNORE_INVALID_OPTIONS = False\r
+ try:\r
+ assert_raises(ValueError, setattr, stuff, 'order_by', '-name,made-up-column')\r
+ assert_raises(ValueError, setattr, stuff, 'order_by', ('made-up-column',))\r
+ # when a column name is overwritten, the original won't work anymore\r
+ assert_raises(ValueError, setattr, stuff, 'order_by', 'c')\r
+ # reset for future tests\r
+ finally:\r
+ tables.options.IGNORE_INVALID_OPTIONS = True\r
+\r
+def test_caches():\r
+ """Ensure the various caches are effective.\r
+ """\r
+\r
+ class BookTable(tables.MemoryTable):\r
+ name = tables.Column()\r
+ answer = tables.Column(default=42)\r
+ books = BookTable([\r
+ {'name': 'Foo: Bar'},\r
+ ])\r
+\r
+ assert id(list(books.columns)[0]) == id(list(books.columns)[0])\r
+ # TODO: row cache currently not used\r
+ #assert id(list(books.rows)[0]) == id(list(books.rows)[0])\r
+\r
+ # test that caches are reset after an update()\r
+ old_column_cache = id(list(books.columns)[0])\r
+ old_row_cache = id(list(books.rows)[0])\r
+ books.update()\r
+ assert id(list(books.columns)[0]) != old_column_cache\r
+ assert id(list(books.rows)[0]) != old_row_cache\r
+\r
+def test_meta_sortable():\r
+ """Specific tests for sortable table meta option."""\r
+\r
+ def mktable(default_sortable):\r
+ class BookTable(tables.MemoryTable):\r
+ id = tables.Column(sortable=True)\r
+ name = tables.Column(sortable=False)\r
+ author = tables.Column()\r
+ class Meta:\r
+ sortable = default_sortable\r
+ return BookTable([])\r
+\r
+ global_table = mktable(None)\r
+ for default_sortable, results in (\r
+ (None, (True, False, True)), # last bool is global default\r
+ (True, (True, False, True)), # last bool is table default\r
+ (False, (True, False, False)), # last bool is table default\r
+ ):\r
+ books = mktable(default_sortable)\r
+ assert [c.sortable for c in books.columns] == list(results)\r
+\r
+ # it also works if the meta option is manually changed after\r
+ # class and instance creation\r
+ global_table._meta.sortable = default_sortable\r
+ assert [c.sortable for c in global_table.columns] == list(results)\r
+\r
+def test_sort():\r
+ class BookTable(tables.MemoryTable):\r
+ id = tables.Column(direction='desc')\r
+ name = tables.Column()\r
+ pages = tables.Column(name='num_pages') # test rewritten names\r
+ language = tables.Column(default='en') # default affects sorting\r
+ rating = tables.Column(data='*') # test data field option\r
+\r
+ books = BookTable([\r
+ {'id': 1, 'pages': 60, 'name': 'Z: The Book', '*': 5}, # language: en\r
+ {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book', '*': 2},\r
+ {'id': 3, 'pages': 80, 'language': 'de', 'name': 'A: The Book, Vol. 2', '*': 4},\r
+ {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'}, # rating (with data option) is missing\r
+ ])\r
+\r
+ # None is normalized to an empty order by tuple, ensuring iterability;\r
+ # it also supports all the cool methods that we offer for order_by.\r
+ # This is true for the default case...\r
+ assert books.order_by == ()\r
+ iter(books.order_by)\r
+ assert hasattr(books.order_by, 'toggle')\r
+ # ...as well as when explicitly set to None.\r
+ books.order_by = None\r
+ assert books.order_by == ()\r
+ iter(books.order_by)\r
+ assert hasattr(books.order_by, 'toggle')\r
+\r
+ # test various orderings\r
+ def test_order(order, result):\r
+ books.order_by = order\r
+ assert [b['id'] for b in books.rows] == result\r
+ test_order(('num_pages',), [1,3,2,4])\r
+ test_order(('-num_pages',), [4,2,3,1])\r
+ test_order(('name',), [2,4,3,1])\r
+ test_order(('language', 'num_pages'), [3,2,1,4])\r
+ # using a simple string (for convinience as well as querystring passing\r
+ test_order('-num_pages', [4,2,3,1])\r
+ test_order('language,num_pages', [3,2,1,4])\r
+ # if overwritten, the declared fieldname has no effect\r
+ test_order('pages,name', [2,4,3,1]) # == ('name',)\r
+ # sort by column with "data" option\r
+ test_order('rating', [4,2,3,1])\r
+\r
+ # test the column with a default ``direction`` set to descending\r
+ test_order('id', [4,3,2,1])\r
+ test_order('-id', [1,2,3,4])\r
+ # changing the direction afterwards is fine too\r
+ books.base_columns['id'].direction = 'asc'\r
+ test_order('id', [1,2,3,4])\r
+ test_order('-id', [4,3,2,1])\r
+ # a invalid direction string raises an exception\r
+ assert_raises(ValueError, setattr, books.base_columns['id'], 'direction', 'blub')\r
+\r
+ # [bug] test alternative order formats if passed to constructor\r
+ BookTable([], 'language,-num_pages')\r
+\r
+ # test invalid order instructions\r
+ books.order_by = 'xyz'\r
+ assert not books.order_by\r
+ books.base_columns['language'].sortable = False\r
+ books.order_by = 'language'\r
+ assert not books.order_by\r
+ test_order(('language', 'num_pages'), [1,3,2,4]) # as if: 'num_pages'\r
+\r
+ # [bug] order_by did not run through setter when passed to init\r
+ books = BookTable([], order_by='name')\r
+ assert books.order_by == ('name',)\r
+\r
+ # test table.order_by extensions\r
+ books.order_by = ''\r
+ assert books.order_by.polarize(False) == ()\r
+ assert books.order_by.polarize(True) == ()\r
+ assert books.order_by.toggle() == ()\r
+ assert books.order_by.polarize(False, ['id']) == ('id',)\r
+ assert books.order_by.polarize(True, ['id']) == ('-id',)\r
+ assert books.order_by.toggle(['id']) == ('id',)\r
+ books.order_by = 'id,-name'\r
+ assert books.order_by.polarize(False, ['name']) == ('id', 'name')\r
+ assert books.order_by.polarize(True, ['name']) == ('id', '-name')\r
+ assert books.order_by.toggle(['name']) == ('id', 'name')\r
+ # ``in`` operator works\r
+ books.order_by = 'name'\r
+ assert 'name' in books.order_by\r
+ books.order_by = '-name'\r
+ assert 'name' in books.order_by\r
+ assert not 'language' in books.order_by\r
+\r
+def test_callable():\r
+ """Data fields, ``default`` and ``data`` options can be callables.\r
+ """\r
+\r
+ class MathTable(tables.MemoryTable):\r
+ lhs = tables.Column()\r
+ rhs = tables.Column()\r
+ op = tables.Column(default='+')\r
+ sum = tables.Column(default=lambda d: calc(d['op'], d['lhs'], d['rhs']))\r
+ sqrt = tables.Column(data=lambda d: int(sqrt(d['sum'])))\r
+\r
+ math = MathTable([\r
+ {'lhs': 1, 'rhs': lambda x: x['lhs']*3}, # 1+3\r
+ {'lhs': 9, 'rhs': lambda x: x['lhs'], 'op': '/'}, # 9/9\r
+ {'lhs': lambda x: x['rhs']+3, 'rhs': 4, 'op': '-'}, # 7-4\r
+ ])\r
+\r
+ # function is called when queried\r
+ def calc(op, lhs, rhs):\r
+ if op == '+': return lhs+rhs\r
+ elif op == '/': return lhs/rhs\r
+ elif op == '-': return lhs-rhs\r
+ assert [calc(row['op'], row['lhs'], row['rhs']) for row in math] == [4,1,3]\r
+\r
+ # field function is called while sorting\r
+ math.order_by = ('-rhs',)\r
+ assert [row['rhs'] for row in math] == [9,4,3]\r
+\r
+ # default function is called while sorting\r
+ math.order_by = ('sum',)\r
+ assert [row['sum'] for row in math] == [1,3,4]\r
+\r
+ # data function is called while sorting\r
+ math.order_by = ('sqrt',)\r
+ assert [row['sqrt'] for row in math] == [1,1,2]\r
+\r
+\r
+# TODO: all the column stuff might warrant it's own test file\r
+def test_columns():\r
+ """Test Table.columns container functionality.\r
+ """\r
+\r
+ class BookTable(tables.MemoryTable):\r
+ id = tables.Column(sortable=False, visible=False)\r
+ name = tables.Column(sortable=True)\r
+ pages = tables.Column(sortable=True)\r
+ language = tables.Column(sortable=False)\r
+ books = BookTable([])\r
+\r
+ assert list(books.columns.sortable()) == [c for c in books.columns if c.sortable]\r
+\r
+ # .columns iterator only yields visible columns\r
+ assert len(list(books.columns)) == 3\r
+ # visiblity of columns can be changed at instance-time\r
+ books.columns['id'].visible = True\r
+ assert len(list(books.columns)) == 4\r
+\r
+\r
+def test_column_order():\r
+ """Test the order functionality of bound columns.\r
+ """\r
+\r
+ class BookTable(tables.MemoryTable):\r
+ id = tables.Column()\r
+ name = tables.Column()\r
+ pages = tables.Column()\r
+ language = tables.Column()\r
+ books = BookTable([])\r
+\r
+ # the basic name property is a no-brainer\r
+ books.order_by = ''\r
+ assert [c.name for c in books.columns] == ['id','name','pages','language']\r
+\r
+ # name_reversed will always reverse, no matter what\r
+ for test in ['', 'name', '-name']:\r
+ books.order_by = test\r
+ assert [c.name_reversed for c in books.columns] == ['-id','-name','-pages','-language']\r
+\r
+ # name_toggled will always toggle\r
+ books.order_by = ''\r
+ assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']\r
+ books.order_by = 'id'\r
+ assert [c.name_toggled for c in books.columns] == ['-id','name','pages','language']\r
+ books.order_by = '-name'\r
+ assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']\r
+ # other columns in an order_by will be dismissed\r
+ books.order_by = '-id,name'\r
+ assert [c.name_toggled for c in books.columns] == ['id','-name','pages','language']\r
+\r
+ # with multi-column order, this is slightly more complex\r
+ books.order_by = ''\r
+ assert [str(c.order_by) for c in books.columns] == ['id','name','pages','language']\r
+ assert [str(c.order_by_reversed) for c in books.columns] == ['-id','-name','-pages','-language']\r
+ assert [str(c.order_by_toggled) for c in books.columns] == ['id','name','pages','language']\r
+ books.order_by = 'id'\r
+ assert [str(c.order_by) for c in books.columns] == ['id','id,name','id,pages','id,language']\r
+ assert [str(c.order_by_reversed) for c in books.columns] == ['-id','id,-name','id,-pages','id,-language']\r
+ assert [str(c.order_by_toggled) for c in books.columns] == ['-id','id,name','id,pages','id,language']\r
+ books.order_by = '-pages,id'\r
+ assert [str(c.order_by) for c in books.columns] == ['-pages,id','-pages,id,name','pages,id','-pages,id,language']\r
+ assert [str(c.order_by_reversed) for c in books.columns] == ['-pages,-id','-pages,id,-name','-pages,id','-pages,id,-language']\r
+ assert [str(c.order_by_toggled) for c in books.columns] == ['-pages,-id','-pages,id,name','pages,id','-pages,id,language']\r
+\r
+ # querying whether a column is ordered is possible\r
+ books.order_by = ''\r
+ assert [c.is_ordered for c in books.columns] == [False, False, False, False]\r
+ books.order_by = 'name'\r
+ assert [c.is_ordered for c in books.columns] == [False, True, False, False]\r
+ assert [c.is_ordered_reverse for c in books.columns] == [False, False, False, False]\r
+ assert [c.is_ordered_straight for c in books.columns] == [False, True, False, False]\r
+ books.order_by = '-pages'\r
+ assert [c.is_ordered for c in books.columns] == [False, False, True, False]\r
+ assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]\r
+ assert [c.is_ordered_straight for c in books.columns] == [False, False, False, False]\r
+ # and even works with multi-column ordering\r
+ books.order_by = 'id,-pages'\r
+ assert [c.is_ordered for c in books.columns] == [True, False, True, False]\r
+ assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]\r
+ assert [c.is_ordered_straight for c in books.columns] == [True, False, False, False]
\ No newline at end of file
import django_tables as tables\r
\r
def test_order_by():\r
- class BookTable(tables.Table):\r
+ class BookTable(tables.MemoryTable):\r
id = tables.Column()\r
name = tables.Column()\r
books = BookTable([\r
assert str(books.order_by) == 'name,-id'\r
\r
def test_columns_and_rows():\r
- class CountryTable(tables.Table):\r
+ class CountryTable(tables.MemoryTable):\r
name = tables.TextColumn()\r
capital = tables.TextColumn(sortable=False)\r
population = tables.NumberColumn(verbose_name="Population Size")\r
def test_render():\r
"""For good measure, render some actual templates."""\r
\r
- class CountryTable(tables.Table):\r
+ class CountryTable(tables.MemoryTable):\r
name = tables.TextColumn()\r
capital = tables.TextColumn()\r
population = tables.NumberColumn(verbose_name="Population Size")\r
add_to_builtins('django_tables.app.templatetags.tables')\r
\r
# [bug] set url param tag handles an order_by tuple with multiple columns\r
- class MyTable(tables.Table):\r
+ class MyTable(tables.MemoryTable):\r
f1 = tables.Column()\r
f2 = tables.Column()\r
t = Template('{% set_url_param x=table.order_by %}')\r