1 """Test the base table functionality.
\r
3 This includes the core, as well as static data, non-model tables.
\r
6 from math import sqrt
\r
7 from py.test import raises
\r
8 from django.core.paginator import Paginator
\r
9 import django_tables as tables
\r
11 def test_declaration():
\r
13 Test defining tables by declaration.
\r
16 class GeoAreaTable(tables.Table):
\r
17 name = tables.Column()
\r
18 population = tables.Column()
\r
20 assert len(GeoAreaTable.base_columns) == 2
\r
21 assert 'name' in GeoAreaTable.base_columns
\r
22 assert not hasattr(GeoAreaTable, 'name')
\r
24 class CountryTable(GeoAreaTable):
\r
25 capital = tables.Column()
\r
27 assert len(CountryTable.base_columns) == 3
\r
28 assert 'capital' in CountryTable.base_columns
\r
30 # multiple inheritance
\r
31 class AddedMixin(tables.Table):
\r
32 added = tables.Column()
\r
33 class CityTable(GeoAreaTable, AddedMixin):
\r
34 mayer = tables.Column()
\r
36 assert len(CityTable.base_columns) == 4
\r
37 assert 'added' in CityTable.base_columns
\r
39 # modelforms: support switching from a non-model table hierarchy to a
\r
40 # modeltable hierarchy (both base class orders)
\r
41 class StateTable1(tables.ModelTable, GeoAreaTable):
\r
42 motto = tables.Column()
\r
43 class StateTable2(GeoAreaTable, tables.ModelTable):
\r
44 motto = tables.Column()
\r
46 assert len(StateTable1.base_columns) == len(StateTable2.base_columns) == 3
\r
47 assert 'motto' in StateTable1.base_columns
\r
48 assert 'motto' in StateTable2.base_columns
\r
51 class StuffTable(tables.Table):
\r
52 name = tables.Column()
\r
53 answer = tables.Column(default=42)
\r
54 c = tables.Column(name="count", default=1)
\r
55 email = tables.Column(data="@")
\r
56 stuff = StuffTable([
\r
57 {'id': 1, 'name': 'Foo Bar', '@': 'foo@bar.org'},
\r
60 # access without order_by works
\r
64 # make sure BoundColumnn.name always gives us the right thing, whether
\r
65 # the column explicitely defines a name or not.
\r
66 stuff.columns['count'].name == 'count'
\r
67 stuff.columns['answer'].name == 'answer'
\r
69 for r in stuff.rows:
\r
70 # unknown fields are removed/not-accessible
\r
72 assert not 'id' in r
\r
73 # missing data is available as default
\r
74 assert 'answer' in r
\r
75 assert r['answer'] == 42 # note: different from prev. line!
\r
77 # all that still works when name overrides are used
\r
80 assert r['count'] == 1
\r
82 # columns with data= option work fine
\r
83 assert r['email'] == 'foo@bar.org'
\r
85 # try to splice rows by index
\r
86 assert 'name' in stuff.rows[0]
\r
87 assert isinstance(stuff.rows[0:], list)
\r
89 # [bug] splicing the table gives us valid, working rows
\r
90 assert list(stuff[0]) == list(stuff.rows[0])
\r
91 assert stuff[0]['name'] == 'Foo Bar'
\r
93 # changing an instance's base_columns does not change the class
\r
94 assert id(stuff.base_columns) != id(StuffTable.base_columns)
\r
95 stuff.base_columns['test'] = tables.Column()
\r
96 assert not 'test' in StuffTable.base_columns
\r
98 # optionally, exceptions can be raised when input is invalid
\r
99 tables.options.IGNORE_INVALID_OPTIONS = False
\r
100 raises(Exception, "stuff.order_by = '-name,made-up-column'")
\r
101 raises(Exception, "stuff.order_by = ('made-up-column',)")
\r
102 # when a column name is overwritten, the original won't work anymore
\r
103 raises(Exception, "stuff.order_by = 'c'")
\r
104 # reset for future tests
\r
105 tables.options.IGNORE_INVALID_OPTIONS = True
\r
108 """Ensure the various caches are effective.
\r
111 class BookTable(tables.Table):
\r
112 name = tables.Column()
\r
113 answer = tables.Column(default=42)
\r
114 books = BookTable([
\r
115 {'name': 'Foo: Bar'},
\r
118 assert id(list(books.columns)[0]) == id(list(books.columns)[0])
\r
119 # TODO: row cache currently not used
\r
120 #assert id(list(books.rows)[0]) == id(list(books.rows)[0])
\r
122 # test that caches are reset after an update()
\r
123 old_column_cache = id(list(books.columns)[0])
\r
124 old_row_cache = id(list(books.rows)[0])
\r
126 assert id(list(books.columns)[0]) != old_column_cache
\r
127 assert id(list(books.rows)[0]) != old_row_cache
\r
129 def test_meta_sortable():
\r
130 """Specific tests for sortable table meta option."""
\r
132 def mktable(default_sortable):
\r
133 class BookTable(tables.Table):
\r
134 id = tables.Column(sortable=True)
\r
135 name = tables.Column(sortable=False)
\r
136 author = tables.Column()
\r
138 sortable = default_sortable
\r
139 return BookTable([])
\r
141 global_table = mktable(None)
\r
142 for default_sortable, results in (
\r
143 (None, (True, False, True)), # last bool is global default
\r
144 (True, (True, False, True)), # last bool is table default
\r
145 (False, (True, False, False)), # last bool is table default
\r
147 books = mktable(default_sortable)
\r
148 assert [c.sortable for c in books.columns] == list(results)
\r
150 # it also works if the meta option is manually changed after
\r
151 # class and instance creation
\r
152 global_table._meta.sortable = default_sortable
\r
153 assert [c.sortable for c in global_table.columns] == list(results)
\r
156 class BookTable(tables.Table):
\r
157 id = tables.Column(direction='desc')
\r
158 name = tables.Column()
\r
159 pages = tables.Column(name='num_pages') # test rewritten names
\r
160 language = tables.Column(default='en') # default affects sorting
\r
161 rating = tables.Column(data='*') # test data field option
\r
163 books = BookTable([
\r
164 {'id': 1, 'pages': 60, 'name': 'Z: The Book', '*': 5}, # language: en
\r
165 {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book', '*': 2},
\r
166 {'id': 3, 'pages': 80, 'language': 'de', 'name': 'A: The Book, Vol. 2', '*': 4},
\r
167 {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'}, # rating (with data option) is missing
\r
170 # None is normalized to an empty order by tuple, ensuring iterability;
\r
171 # it also supports all the cool methods that we offer for order_by.
\r
172 # This is true for the default case...
\r
173 assert books.order_by == ()
\r
174 iter(books.order_by)
\r
175 assert hasattr(books.order_by, 'toggle')
\r
176 # ...as well as when explicitly set to None.
\r
177 books.order_by = None
\r
178 assert books.order_by == ()
\r
179 iter(books.order_by)
\r
180 assert hasattr(books.order_by, 'toggle')
\r
182 # test various orderings
\r
183 def test_order(order, result):
\r
184 books.order_by = order
\r
185 assert [b['id'] for b in books.rows] == result
\r
186 test_order(('num_pages',), [1,3,2,4])
\r
187 test_order(('-num_pages',), [4,2,3,1])
\r
188 test_order(('name',), [2,4,3,1])
\r
189 test_order(('language', 'num_pages'), [3,2,1,4])
\r
190 # using a simple string (for convinience as well as querystring passing
\r
191 test_order('-num_pages', [4,2,3,1])
\r
192 test_order('language,num_pages', [3,2,1,4])
\r
193 # if overwritten, the declared fieldname has no effect
\r
194 test_order('pages,name', [2,4,3,1]) # == ('name',)
\r
195 # sort by column with "data" option
\r
196 test_order('rating', [4,2,3,1])
\r
198 # test the column with a default ``direction`` set to descending
\r
199 test_order('id', [4,3,2,1])
\r
200 test_order('-id', [1,2,3,4])
\r
201 # changing the direction afterwards is fine too
\r
202 books.base_columns['id'].direction = 'asc'
\r
203 test_order('id', [1,2,3,4])
\r
204 test_order('-id', [4,3,2,1])
\r
205 # a invalid direction string raises an exception
\r
206 raises(ValueError, "books.base_columns['id'].direction = 'blub'")
\r
208 # [bug] test alternative order formats if passed to constructor
\r
209 BookTable([], 'language,-num_pages')
\r
211 # test invalid order instructions
\r
212 books.order_by = 'xyz'
\r
213 assert not books.order_by
\r
214 books.base_columns['language'].sortable = False
\r
215 books.order_by = 'language'
\r
216 assert not books.order_by
\r
217 test_order(('language', 'num_pages'), [1,3,2,4]) # as if: 'num_pages'
\r
219 # [bug] order_by did not run through setter when passed to init
\r
220 books = BookTable([], order_by='name')
\r
221 assert books.order_by == ('name',)
\r
223 # test table.order_by extensions
\r
224 books.order_by = ''
\r
225 assert books.order_by.polarize(False) == ()
\r
226 assert books.order_by.polarize(True) == ()
\r
227 assert books.order_by.toggle() == ()
\r
228 assert books.order_by.polarize(False, ['id']) == ('id',)
\r
229 assert books.order_by.polarize(True, ['id']) == ('-id',)
\r
230 assert books.order_by.toggle(['id']) == ('id',)
\r
231 books.order_by = 'id,-name'
\r
232 assert books.order_by.polarize(False, ['name']) == ('id', 'name')
\r
233 assert books.order_by.polarize(True, ['name']) == ('id', '-name')
\r
234 assert books.order_by.toggle(['name']) == ('id', 'name')
\r
235 # ``in`` operator works
\r
236 books.order_by = 'name'
\r
237 assert 'name' in books.order_by
\r
238 books.order_by = '-name'
\r
239 assert 'name' in books.order_by
\r
240 assert not 'language' in books.order_by
\r
242 def test_callable():
\r
243 """Data fields, ``default`` and ``data`` options can be callables.
\r
246 class MathTable(tables.Table):
\r
247 lhs = tables.Column()
\r
248 rhs = tables.Column()
\r
249 op = tables.Column(default='+')
\r
250 sum = tables.Column(default=lambda d: calc(d['op'], d['lhs'], d['rhs']))
\r
251 sqrt = tables.Column(data=lambda d: int(sqrt(d['sum'])))
\r
254 {'lhs': 1, 'rhs': lambda x: x['lhs']*3}, # 1+3
\r
255 {'lhs': 9, 'rhs': lambda x: x['lhs'], 'op': '/'}, # 9/9
\r
256 {'lhs': lambda x: x['rhs']+3, 'rhs': 4, 'op': '-'}, # 7-4
\r
259 # function is called when queried
\r
260 def calc(op, lhs, rhs):
\r
261 if op == '+': return lhs+rhs
\r
262 elif op == '/': return lhs/rhs
\r
263 elif op == '-': return lhs-rhs
\r
264 assert [calc(row['op'], row['lhs'], row['rhs']) for row in math] == [4,1,3]
\r
266 # field function is called while sorting
\r
267 math.order_by = ('-rhs',)
\r
268 assert [row['rhs'] for row in math] == [9,4,3]
\r
270 # default function is called while sorting
\r
271 math.order_by = ('sum',)
\r
272 assert [row['sum'] for row in math] == [1,3,4]
\r
274 # data function is called while sorting
\r
275 math.order_by = ('sqrt',)
\r
276 assert [row['sqrt'] for row in math] == [1,1,2]
\r
278 def test_pagination():
\r
279 class BookTable(tables.Table):
\r
280 name = tables.Column()
\r
282 # create some sample data
\r
284 for i in range(1,101):
\r
285 data.append({'name': 'Book Nr. %d'%i})
\r
286 books = BookTable(data)
\r
288 # external paginator
\r
289 paginator = Paginator(books.rows, 10)
\r
290 assert paginator.num_pages == 10
\r
291 page = paginator.page(1)
\r
292 assert len(page.object_list) == 10
\r
293 assert page.has_previous() == False
\r
294 assert page.has_next() == True
\r
296 # integrated paginator
\r
297 books.paginate(Paginator, 10, page=1)
\r
298 # rows is now paginated
\r
299 assert len(list(books.rows.page())) == 10
\r
300 assert len(list(books.rows.all())) == 100
\r
302 assert books.paginator.num_pages == 10
\r
303 assert books.page.has_previous() == False
\r
304 assert books.page.has_next() == True
\r
306 # TODO: all the column stuff might warrant it's own test file
\r
307 def test_columns():
\r
308 """Test Table.columns container functionality.
\r
311 class BookTable(tables.Table):
\r
312 id = tables.Column(sortable=False)
\r
313 name = tables.Column(sortable=True)
\r
314 pages = tables.Column(sortable=True)
\r
315 language = tables.Column(sortable=False)
\r
316 books = BookTable([])
\r
318 assert list(books.columns.sortable()) == [c for c in books.columns if c.sortable]
\r
321 def test_column_order():
\r
322 """Test the order functionality of bound columns.
\r
325 class BookTable(tables.Table):
\r
326 id = tables.Column()
\r
327 name = tables.Column()
\r
328 pages = tables.Column()
\r
329 language = tables.Column()
\r
330 books = BookTable([])
\r
332 # the basic name property is a no-brainer
\r
333 books.order_by = ''
\r
334 assert [c.name for c in books.columns] == ['id','name','pages','language']
\r
336 # name_reversed will always reverse, no matter what
\r
337 for test in ['', 'name', '-name']:
\r
338 books.order_by = test
\r
339 assert [c.name_reversed for c in books.columns] == ['-id','-name','-pages','-language']
\r
341 # name_toggled will always toggle
\r
342 books.order_by = ''
\r
343 assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']
\r
344 books.order_by = 'id'
\r
345 assert [c.name_toggled for c in books.columns] == ['-id','name','pages','language']
\r
346 books.order_by = '-name'
\r
347 assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']
\r
348 # other columns in an order_by will be dismissed
\r
349 books.order_by = '-id,name'
\r
350 assert [c.name_toggled for c in books.columns] == ['id','-name','pages','language']
\r
352 # with multi-column order, this is slightly more complex
\r
353 books.order_by = ''
\r
354 assert [str(c.order_by) for c in books.columns] == ['id','name','pages','language']
\r
355 assert [str(c.order_by_reversed) for c in books.columns] == ['-id','-name','-pages','-language']
\r
356 assert [str(c.order_by_toggled) for c in books.columns] == ['id','name','pages','language']
\r
357 books.order_by = 'id'
\r
358 assert [str(c.order_by) for c in books.columns] == ['id','id,name','id,pages','id,language']
\r
359 assert [str(c.order_by_reversed) for c in books.columns] == ['-id','id,-name','id,-pages','id,-language']
\r
360 assert [str(c.order_by_toggled) for c in books.columns] == ['-id','id,name','id,pages','id,language']
\r
361 books.order_by = '-pages,id'
\r
362 assert [str(c.order_by) for c in books.columns] == ['-pages,id','-pages,id,name','pages,id','-pages,id,language']
\r
363 assert [str(c.order_by_reversed) for c in books.columns] == ['-pages,-id','-pages,id,-name','-pages,id','-pages,id,-language']
\r
364 assert [str(c.order_by_toggled) for c in books.columns] == ['-pages,-id','-pages,id,name','pages,id','-pages,id,language']
\r
366 # querying whether a column is ordered is possible
\r
367 books.order_by = ''
\r
368 assert [c.is_ordered for c in books.columns] == [False, False, False, False]
\r
369 books.order_by = 'name'
\r
370 assert [c.is_ordered for c in books.columns] == [False, True, False, False]
\r
371 assert [c.is_ordered_reverse for c in books.columns] == [False, False, False, False]
\r
372 assert [c.is_ordered_straight for c in books.columns] == [False, True, False, False]
\r
373 books.order_by = '-pages'
\r
374 assert [c.is_ordered for c in books.columns] == [False, False, True, False]
\r
375 assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]
\r
376 assert [c.is_ordered_straight for c in books.columns] == [False, False, False, False]
\r
377 # and even works with multi-column ordering
\r
378 books.order_by = 'id,-pages'
\r
379 assert [c.is_ordered for c in books.columns] == [True, False, True, False]
\r
380 assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]
\r
381 assert [c.is_ordered_straight for c in books.columns] == [True, False, False, False]