From: Bradley Ayers Date: Thu, 24 Mar 2011 03:07:49 +0000 (+1000) Subject: * Moved tests out of the app, and integrated them with setup.py X-Git-Tag: v0.4.0.alpha4^2~5 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=11451794ec659b8ccd8bc24327dd45d5f096f6fe;p=django-tables2.git * Moved tests out of the app, and integrated them with setup.py * Added more tests for QuerySet data sources * Clarified documentation * Renamed BoundRow.data to BoundRow.record --- diff --git a/.gitignore b/.gitignore index 5ce8003..6eabac0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.pyc /*.komodoproject /*.egg-info/ +/*.egg /MANIFEST /dist/ /build/ diff --git a/django_tables/rows.py b/django_tables/rows.py index 9544bd3..a6f9ed9 100644 --- a/django_tables/rows.py +++ b/django_tables/rows.py @@ -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. diff --git a/django_tables/tables.py b/django_tables/tables.py index 09062b8..2897e50 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -112,7 +112,7 @@ class TableData(object): 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() diff --git a/django_tables/templatetags/django_tables.py b/django_tables/templatetags/django_tables.py index db15886..b914f4e 100644 --- a/django_tables/templatetags/django_tables.py +++ b/django_tables/templatetags/django_tables.py @@ -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 index 5572945..0000000 --- a/django_tables/tests/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from attest import Tests -from .core import core -from .templates import templates -from .models import models -#from .memory import memory - - -tests = Tests([core, templates, models]) - -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 index 20d52ac..0000000 --- a/django_tables/tests/memory.py +++ /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 index 4bc010b..0000000 --- a/django_tables/tests/models.py +++ /dev/null @@ -1,392 +0,0 @@ -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): - table = samples.UserTable(User.objects.all()) - - -''' -class City(models.Model): - name = models.TextField() - population = models.IntegerField(null=True) - - class Meta: - app_label = 'django_tables' -django_tables_models.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) # - " - - - class Meta: - app_label = 'django_tables' - - def example_domain(self): - return 'example.%s' % self.tld -django_tables_models.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/docs/conf.py b/docs/conf.py index 2ebada6..93ad764 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ project = u'django-tables' # built documents. # # The short X.Y version. -version = '0.4.0.alpha2.dev' +version = '0.4.0.alpha2' # The full version, including alpha/beta/rc tags. -release = '0.4.0.alpha2.dev' +release = '0.4.0.alpha2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index 03d3b1f..7cbe396 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -440,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. diff --git a/setup.py b/setup.py index f1cd288..bd4e54b 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages setup( name='django-tables', - version='0.4.0.alpha2.dev', + version='0.4.0.alpha2', description='Table framework for Django', author='Bradley Ayers', @@ -16,6 +16,10 @@ setup( include_package_data=True, # declarations in MANIFEST.in install_requires=['Django >=1.1'], + tests_require=['Django >=1.1', 'Attest >=0.4', 'django_attest'], + + test_loader='attest:FancyReporter.test_loader', + test_suite='tests.everything', classifiers=[ 'Environment :: Web Environment', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..602f2c4 --- /dev/null +++ b/tests/__init__.py @@ -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]) diff --git a/django_tables/tests/core.py b/tests/core.py similarity index 99% rename from django_tables/tests/core.py rename to tests/core.py index f22d7b2..93fbd2d 100644 --- a/django_tables/tests/core.py +++ b/tests/core.py @@ -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 index 0000000..7a4c9d4 --- /dev/null +++ b/tests/models.py @@ -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'] diff --git a/django_tables/tests/templates.py b/tests/templates.py similarity index 100% rename from django_tables/tests/templates.py rename to tests/templates.py