minor comment spelling mistake
[django-tables2.git] / tests / test_basic.py
1 """Test the base table functionality.\r
2 \r
3 This includes the core, as well as static data, non-model tables.\r
4 """\r
5 \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
10 \r
11 def test_declaration():\r
12     """\r
13     Test defining tables by declaration.\r
14     """\r
15 \r
16     class GeoAreaTable(tables.Table):\r
17         name = tables.Column()\r
18         population = tables.Column()\r
19 \r
20     assert len(GeoAreaTable.base_columns) == 2\r
21     assert 'name' in GeoAreaTable.base_columns\r
22     assert not hasattr(GeoAreaTable, 'name')\r
23 \r
24     class CountryTable(GeoAreaTable):\r
25         capital = tables.Column()\r
26 \r
27     assert len(CountryTable.base_columns) == 3\r
28     assert 'capital' in CountryTable.base_columns\r
29 \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
35 \r
36     assert len(CityTable.base_columns) == 4\r
37     assert 'added' in CityTable.base_columns\r
38 \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
45 \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
49 \r
50 def test_basic():\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
58     ])\r
59 \r
60     # access without order_by works\r
61     stuff.data\r
62     stuff.rows\r
63 \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
68 \r
69     for r in stuff.rows:\r
70         # unknown fields are removed/not-accessible\r
71         assert 'name' in r\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
76 \r
77         # all that still works when name overrides are used\r
78         assert not 'c' in r\r
79         assert 'count' in r\r
80         assert r['count'] == 1\r
81 \r
82         # columns with data= option work fine\r
83         assert r['email'] == 'foo@bar.org'\r
84 \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
88 \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
92 \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
97 \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
106 \r
107 def test_caches():\r
108     """Ensure the various caches are effective.\r
109     """\r
110 \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
116     ])\r
117 \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
121 \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
125     books.update()\r
126     assert id(list(books.columns)[0]) != old_column_cache\r
127     assert id(list(books.rows)[0]) != old_row_cache\r
128 \r
129 def test_meta_sortable():\r
130     """Specific tests for sortable table meta option."""\r
131 \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
137             class Meta:\r
138                 sortable = default_sortable\r
139         return BookTable([])\r
140 \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
146     ):\r
147         books = mktable(default_sortable)\r
148         assert [c.sortable for c in books.columns] == list(results)\r
149 \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
154 \r
155 def test_sort():\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
162 \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
168     ])\r
169 \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
181 \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
197 \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
207 \r
208     # [bug] test alternative order formats if passed to constructor\r
209     BookTable([], 'language,-num_pages')\r
210 \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
218 \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
222 \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
241 \r
242 def test_callable():\r
243     """Data fields, ``default`` and ``data`` options can be callables.\r
244     """\r
245 \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
252 \r
253     math = MathTable([\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
257     ])\r
258 \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
265 \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
269 \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
273 \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
277 \r
278 def test_pagination():\r
279     class BookTable(tables.Table):\r
280         name = tables.Column()\r
281 \r
282     # create some sample data\r
283     data = []\r
284     for i in range(1,101):\r
285         data.append({'name': 'Book Nr. %d'%i})\r
286     books = BookTable(data)\r
287 \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
295 \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
301     # new attributes\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
305 \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
309     """\r
310 \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
317 \r
318     assert list(books.columns.sortable()) == [c for c in books.columns if c.sortable]\r
319 \r
320 \r
321 def test_column_order():\r
322     """Test the order functionality of bound columns.\r
323     """\r
324 \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
331 \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
335 \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
340 \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
351 \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
365 \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]