*.pyc
/*.komodoproject
+/*.egg-info/
+/*.egg
/MANIFEST
/dist/
/build/
/docs/_build/
-/django_tables.egg-info/
--- /dev/null
+include README.rst
+recursive-include django_tables/templates *
-# -*- 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 *
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.
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
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.
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"""
"""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
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()
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()
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
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
"""
-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``.
{% set_url_param page=next_page %}
{% set_url_param page="" %}
{% set_url_param filter="books" page=1 %}
-"""
+"""
import urllib
import tokenize
import StringIO
from django.template.loader import get_template
from django.utils.safestring import mark_safe
+
register = template.Library()
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:]:
"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):
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)
+++ /dev/null
-from attest import Tests
-from .core import core
-from .templates import templates
-#from .memory import memory
-#from .models import models
-
-tests = Tests([core, templates])
-
-def suite():
- return tests.test_suite()
-
-if __name__ == '__main__':
- tests.main()
+++ /dev/null
-"""Test the 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]
+++ /dev/null
-"""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
-'''
+++ /dev/null
-"""Empty demo app our tests can assign models to."""
# 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.
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
#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
-------------------------------
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).
<[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
---------------
</table>
+.. _subclassing-column:
+
Subclassing :class:`Column`
---------------------------
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
-------------------------
.. 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.
-# -*- 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',
'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']
)
--- /dev/null
+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])
"""Test the core table functionality."""
-
import copy
from attest import Tests, Assert
from django.http import Http404
--- /dev/null
+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']