1 """Test ModelTable specific functionality.
3 Sets up a temporary Django project using a memory SQLite database.
6 from nose.tools import assert_raises, assert_equal
7 from django.conf import settings
8 from django.core.paginator import *
9 import django_tables as tables
12 def setup_module(module):
13 settings.configure(**{
14 'DATABASE_ENGINE': 'sqlite3',
15 'DATABASE_NAME': ':memory:',
16 'INSTALLED_APPS': ('tests.testapp',)
19 from django.db import models
20 from django.core.management import call_command
22 class City(models.Model):
23 name = models.TextField()
24 population = models.IntegerField(null=True)
29 class Country(models.Model):
30 name = models.TextField()
31 population = models.IntegerField()
32 capital = models.ForeignKey(City, blank=True, null=True)
33 tld = models.TextField(verbose_name='Domain Extension', max_length=2)
34 system = models.TextField(blank=True, null=True)
35 null = models.TextField(blank=True, null=True) # tests expect this to be always null!
36 null2 = models.TextField(blank=True, null=True) # - " -
37 def example_domain(self):
38 return 'example.%s' % self.tld
41 module.Country = Country
44 call_command('syncdb', verbosity=1, interactive=False)
46 # create a couple of objects
47 berlin=City(name="Berlin"); berlin.save()
48 amsterdam=City(name="Amsterdam"); amsterdam.save()
49 Country(name="Austria", tld="au", population=8, system="republic").save()
50 Country(name="Germany", tld="de", population=81, capital=berlin).save()
51 Country(name="France", tld="fr", population=64, system="republic").save()
52 Country(name="Netherlands", tld="nl", population=16, system="monarchy", capital=amsterdam).save()
55 class TestDeclaration:
56 """Test declaration, declared columns and default model field columns.
59 def test_autogen_basic(self):
60 class CountryTable(tables.ModelTable):
64 assert len(CountryTable.base_columns) == 8
65 assert 'name' in CountryTable.base_columns
66 assert not hasattr(CountryTable, 'name')
68 # Override one model column, add another custom one, exclude one
69 class CountryTable(tables.ModelTable):
70 capital = tables.TextColumn(verbose_name='Name of capital')
71 projected = tables.Column(verbose_name="Projected Population")
76 assert len(CountryTable.base_columns) == 8
77 assert 'projected' in CountryTable.base_columns
78 assert 'capital' in CountryTable.base_columns
79 assert not 'tld' in CountryTable.base_columns
81 # Inheritance (with a different model) + field restrictions
82 class CityTable(CountryTable):
85 columns = ['id', 'name']
88 print CityTable.base_columns
89 assert len(CityTable.base_columns) == 4
90 assert 'id' in CityTable.base_columns
91 assert 'name' in CityTable.base_columns
92 assert 'projected' in CityTable.base_columns # declared in parent
93 assert not 'population' in CityTable.base_columns # not in Meta:columns
94 assert 'capital' in CityTable.base_columns # in exclude, but only works on model fields (is that the right behaviour?)
96 def test_columns_custom_order(self):
97 """Using the columns meta option, you can also modify the ordering.
99 class CountryTable(tables.ModelTable):
100 foo = tables.Column()
103 columns = ('system', 'population', 'foo', 'tld',)
105 assert [c.name for c in CountryTable().columns] == ['system', 'population', 'foo', 'tld']
109 """Some tests here are copied from ``test_basic.py`` but need to be
110 rerun with a ModelTable, as the implementation is different."""
112 class CountryTable(tables.ModelTable):
113 null = tables.Column(default="foo")
114 tld = tables.Column(name="domain")
118 countries = CountryTable()
120 def test_country_table(table):
122 # "normal" fields exist
124 # unknown fields are removed/not accessible
125 assert not 'does-not-exist' in r
126 # ...so are excluded fields
128 # [bug] access to data that might be available, but does not
129 # have a corresponding column is denied.
130 assert_raises(Exception, "r['id']")
131 # missing data is available with default values
133 assert r['null'] == "foo" # note: different from prev. line!
134 # if everything else fails (no default), we get None back
135 assert r['null2'] is None
137 # all that still works when name overrides are used
138 assert not 'tld' in r
140 assert len(r['domain']) == 2 # valid country tld
141 test_country_table(countries)
143 # repeat the avove tests with a table that is not associated with a
144 # model, and all columns being created manually.
145 class CountryTable(tables.ModelTable):
146 name = tables.Column()
147 population = tables.Column()
148 capital = tables.Column()
149 system = tables.Column()
150 null = tables.Column(default="foo")
151 null2 = tables.Column()
152 tld = tables.Column(name="domain")
153 countries = CountryTable(Country)
154 test_country_table(countries)
157 def test_invalid_accessor():
158 """Test that a column being backed by a non-existent model property
159 is handled correctly.
161 Regression-Test: There used to be a NameError here.
163 class CountryTable(tables.ModelTable):
164 name = tables.Column(data='something-i-made-up')
165 countries = CountryTable(Country)
166 assert_raises(ValueError, countries[0].__getitem__, 'name')
170 """Make sure the caches work for model tables as well (parts are
173 class CountryTable(tables.ModelTable):
177 countries = CountryTable()
179 assert id(list(countries.columns)[0]) == id(list(countries.columns)[0])
180 # TODO: row cache currently not used
181 #assert id(list(countries.rows)[0]) == id(list(countries.rows)[0])
183 # test that caches are reset after an update()
184 old_column_cache = id(list(countries.columns)[0])
185 old_row_cache = id(list(countries.rows)[0])
187 assert id(list(countries.columns)[0]) != old_column_cache
188 assert id(list(countries.rows)[0]) != old_row_cache
191 class CountryTable(tables.ModelTable):
192 tld = tables.Column(name="domain")
193 population = tables.Column()
194 system = tables.Column(default="republic")
195 custom1 = tables.Column()
196 custom2 = tables.Column(sortable=True)
199 countries = CountryTable()
201 def test_order(order, result, table=countries):
202 table.order_by = order
203 assert [r['id'] for r in table.rows] == result
205 # test various orderings
206 test_order(('population',), [1,4,3,2])
207 test_order(('-population',), [2,3,4,1])
208 test_order(('name',), [1,3,2,4])
209 # test sorting with a "rewritten" column name
210 countries.order_by = 'domain,tld' # "tld" would be invalid...
211 countries.order_by == ('domain',) # ...and is therefore removed
212 test_order(('-domain',), [4,3,2,1])
213 # test multiple order instructions; note: one row is missing a "system"
214 # value, but has a default set; however, that has no effect on sorting.
215 test_order(('system', '-population'), [2,4,3,1])
216 # using a simple string (for convinience as well as querystring passing)
217 test_order('-population', [2,3,4,1])
218 test_order('system,-population', [2,4,3,1])
220 # test column with a default ``direction`` set to descending
221 class CityTable(tables.ModelTable):
222 name = tables.Column(direction='desc')
226 test_order('name', [1,2], table=cities) # Berlin to Amsterdam
227 test_order('-name', [2,1], table=cities) # Amsterdam to Berlin
229 # test invalid order instructions...
230 countries.order_by = 'invalid_field,population'
231 assert countries.order_by == ('population',)
232 # ...in case of ModelTables, this primarily means that only
233 # model-based colunns are currently sortable at all.
234 countries.order_by = ('custom1', 'custom2')
235 assert countries.order_by == ()
237 def test_default_sort():
238 class SortedCountryTable(tables.ModelTable):
243 # the order_by option is provided by TableOptions
244 assert_equal('-name', SortedCountryTable()._meta.order_by)
246 # the default order can be inherited from the table
247 assert_equal(('-name',), SortedCountryTable().order_by)
248 assert_equal(4, SortedCountryTable().rows[0]['id'])
250 # and explicitly set (or reset) via __init__
251 assert_equal(2, SortedCountryTable(order_by='system').rows[0]['id'])
252 assert_equal(1, SortedCountryTable(order_by=None).rows[0]['id'])
255 """Some of the callable code is reimplemented for modeltables, so
256 test some specifics again.
259 class CountryTable(tables.ModelTable):
260 null = tables.Column(default=lambda s: s['example_domain'])
261 example_domain = tables.Column()
264 countries = CountryTable(Country)
266 # model method is called
267 assert [row['example_domain'] for row in countries] == \
268 ['example.'+row['tld'] for row in countries]
270 # column default method is called
271 assert [row['example_domain'] for row in countries] == \
272 [row['null'] for row in countries]
275 def test_relationships():
276 """Test relationship spanning."""
278 class CountryTable(tables.ModelTable):
279 # add relationship spanning columns (using different approaches)
280 capital_name = tables.Column(data='capital__name')
281 capital__population = tables.Column(name="capital_population")
282 invalid = tables.Column(data="capital__invalid")
285 countries = CountryTable(Country.objects.select_related('capital'))
287 # ordering and field access works
288 countries.order_by = 'capital_name'
289 assert [row['capital_name'] for row in countries.rows] == \
290 [None, None, 'Amsterdam', 'Berlin']
292 countries.order_by = 'capital_population'
293 assert [row['capital_population'] for row in countries.rows] == \
294 [None, None, None, None]
296 # ordering by a column with an invalid relationship fails silently
297 countries.order_by = 'invalid'
298 assert countries.order_by == ()
301 def test_pagination():
302 """Pretty much the same as static table pagination, but make sure we
303 provide the capability, at least for paginators that use it, to not
304 have the complete queryset loaded (by use of a count() query).
306 Note: This test changes the available cities, make sure it is last,
307 or that tests that follow are written appropriately.
309 from django.db import connection
311 class CityTable(tables.ModelTable):
317 # add some sample data
318 City.objects.all().delete()
319 for i in range(1,101):
320 City.objects.create(name="City %d"%i)
323 settings.DEBUG = True
326 start_querycount = len(connection.queries)
327 paginator = Paginator(cities.rows, 10)
328 assert paginator.num_pages == 10
329 page = paginator.page(1)
330 assert len(page.object_list) == 10
331 assert page.has_previous() == False
332 assert page.has_next() == True
333 # Make sure the queryset is not loaded completely - there must be two
334 # queries, one a count(). This check is far from foolproof...
335 assert len(connection.queries)-start_querycount == 2
337 # using a queryset paginator is possible as well (although unnecessary)
338 paginator = QuerySetPaginator(cities.rows, 10)
339 assert paginator.num_pages == 10
341 # integrated paginator
342 start_querycount = len(connection.queries)
343 cities.paginate(Paginator, 10, page=1)
344 # rows is now paginated
345 assert len(list(cities.rows.page())) == 10
346 assert len(list(cities.rows.all())) == 100
348 assert cities.paginator.num_pages == 10
349 assert cities.page.has_previous() == False
350 assert cities.page.has_next() == True
351 assert len(connection.queries)-start_querycount == 2
354 settings.DEBUG = False