Fixed a NameError. This potentially closes issue 5.
[django-tables2.git] / tests / test_models.py
1 """Test ModelTable specific functionality.
2
3 Sets up a temporary Django project using a memory SQLite database.
4 """
5
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
10
11
12 def setup_module(module):
13     settings.configure(**{
14         'DATABASE_ENGINE': 'sqlite3',
15         'DATABASE_NAME': ':memory:',
16         'INSTALLED_APPS': ('tests.testapp',)
17     })
18
19     from django.db import models
20     from django.core.management import call_command
21
22     class City(models.Model):
23         name = models.TextField()
24         population = models.IntegerField(null=True)
25         class Meta:
26             app_label = 'testapp'
27     module.City = City
28
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
39         class Meta:
40             app_label = 'testapp'
41     module.Country = Country
42
43     # create the tables
44     call_command('syncdb', verbosity=1, interactive=False)
45
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()
53
54
55 class TestDeclaration:
56     """Test declaration, declared columns and default model field columns.
57     """
58
59     def test_autogen_basic(self):
60         class CountryTable(tables.ModelTable):
61             class Meta:
62                 model = Country
63
64         assert len(CountryTable.base_columns) == 8
65         assert 'name' in CountryTable.base_columns
66         assert not hasattr(CountryTable, 'name')
67
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")
72             class Meta:
73                 model = Country
74                 exclude = ['tld']
75
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
80
81         # Inheritance (with a different model) + field restrictions
82         class CityTable(CountryTable):
83             class Meta:
84                 model = City
85                 columns = ['id', 'name']
86                 exclude = ['capital']
87
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?)
95
96     def test_columns_custom_order(self):
97         """Using the columns meta option, you can also modify the ordering.
98         """
99         class CountryTable(tables.ModelTable):
100             foo = tables.Column()
101             class Meta:
102                 model = Country
103                 columns = ('system', 'population', 'foo', 'tld',)
104
105         assert [c.name for c in CountryTable().columns] == ['system', 'population', 'foo', 'tld']
106
107
108 def test_basic():
109     """Some tests here are copied from ``test_basic.py`` but need to be
110     rerun with a ModelTable, as the implementation is different."""
111
112     class CountryTable(tables.ModelTable):
113         null = tables.Column(default="foo")
114         tld = tables.Column(name="domain")
115         class Meta:
116             model = Country
117             exclude = ('id',)
118     countries = CountryTable()
119
120     def test_country_table(table):
121         for r in table.rows:
122             # "normal" fields exist
123             assert 'name' in r
124             # unknown fields are removed/not accessible
125             assert not 'does-not-exist' in r
126             # ...so are excluded fields
127             assert not 'id' in r
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
132             assert 'null' in r
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
136
137             # all that still works when name overrides are used
138             assert not 'tld' in r
139             assert 'domain' in r
140             assert len(r['domain']) == 2   # valid country tld
141     test_country_table(countries)
142
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)
155
156
157 def test_invalid_accessor():
158     """Test that a column being backed by a non-existent model property
159     is handled correctly.
160
161     Regression-Test: There used to be a NameError here.
162     """
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')
167
168
169 def test_caches():
170     """Make sure the caches work for model tables as well (parts are
171     reimplemented).
172     """
173     class CountryTable(tables.ModelTable):
174         class Meta:
175             model = Country
176             exclude = ('id',)
177     countries = CountryTable()
178
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])
182
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])
186     countries.update()
187     assert id(list(countries.columns)[0]) != old_column_cache
188     assert id(list(countries.rows)[0]) != old_row_cache
189
190 def test_sort():
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)
197         class Meta:
198             model = Country
199     countries = CountryTable()
200
201     def test_order(order, result, table=countries):
202         table.order_by = order
203         assert [r['id'] for r in table.rows] == result
204
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])
219
220     # test column with a default ``direction`` set to descending
221     class CityTable(tables.ModelTable):
222         name = tables.Column(direction='desc')
223         class Meta:
224             model = City
225     cities = CityTable()
226     test_order('name', [1,2], table=cities)   # Berlin to Amsterdam
227     test_order('-name', [2,1], table=cities)  # Amsterdam to Berlin
228
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 == ()
236
237 def test_default_sort():
238     class SortedCountryTable(tables.ModelTable):
239         class Meta:
240             model = Country
241             order_by = '-name'
242
243     # the order_by option is provided by TableOptions
244     assert_equal('-name', SortedCountryTable()._meta.order_by)
245
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'])
249
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'])
253
254 def test_callable():
255     """Some of the callable code is reimplemented for modeltables, so
256     test some specifics again.
257     """
258
259     class CountryTable(tables.ModelTable):
260         null = tables.Column(default=lambda s: s['example_domain'])
261         example_domain = tables.Column()
262         class Meta:
263             model = Country
264     countries = CountryTable(Country)
265
266     # model method is called
267     assert [row['example_domain'] for row in countries] == \
268                     ['example.'+row['tld'] for row in countries]
269
270     # column default method is called
271     assert [row['example_domain'] for row in countries] == \
272                     [row['null'] for row in countries]
273
274
275 def test_relationships():
276     """Test relationship spanning."""
277
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")
283         class Meta:
284             model = Country
285     countries = CountryTable(Country.objects.select_related('capital'))
286
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']
291
292     countries.order_by = 'capital_population'
293     assert [row['capital_population'] for row in countries.rows] == \
294         [None, None, None, None]
295
296     # ordering by a column with an invalid relationship fails silently
297     countries.order_by = 'invalid'
298     assert countries.order_by == ()
299
300
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).
305
306     Note: This test changes the available cities, make sure it is last,
307     or that tests that follow are written appropriately.
308     """
309     from django.db import connection
310
311     class CityTable(tables.ModelTable):
312         class Meta:
313             model = City
314             columns = ['name']
315     cities = CityTable()
316
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)
321
322     # for query logging
323     settings.DEBUG = True
324
325     # external paginator
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
336
337     # using a queryset paginator is possible as well (although unnecessary)
338     paginator = QuerySetPaginator(cities.rows, 10)
339     assert paginator.num_pages == 10
340
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
347     # new attributes
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
352
353     # reset
354     settings.DEBUG = False