TODOs.
[django-tables2.git] / README
1 django-tables\r
2 =============\r
3 \r
4 A Django QuerySet renderer.\r
5 \r
6 Installation\r
7 ------------\r
8 \r
9 Adding django-tables to your INSTALLED_APPS settings is optional, it'll get\r
10 you the ability to load some template utilities via {% load tables %}, but\r
11 apart from that, ``import django_tables as tables`` should get you going.\r
12 \r
13 Running the test suite\r
14 ----------------------\r
15 \r
16 The test suite uses nose:\r
17     http://somethingaboutorange.com/mrl/projects/nose/\r
18 \r
19 Working with Tables\r
20 -------------------\r
21 \r
22 A table class looks very much like a form:\r
23 \r
24     import django_tables as tables\r
25     class CountryTable(tables.MemoryTable):\r
26         name = tables.Column(verbose_name="Country Name")\r
27         population = tables.Column(sortable=False, visible=False)\r
28         time_zone = tables.Column(name="tz", default="UTC+1")\r
29 \r
30 Instead of fields, you declare a column for every piece of data you want to\r
31 expose to the user.\r
32 \r
33 To use the table, create an instance:\r
34 \r
35     countries = CountryTable([{'name': 'Germany', population: 80},\r
36                               {'name': 'France', population: 64}])\r
37 \r
38 Decide how the table should be sorted:\r
39 \r
40     countries.order_by = ('name',)\r
41     assert [row.name for row in countries.row] == ['France', 'Germany']\r
42 \r
43     countries.order_by = ('-population',)\r
44     assert [row.name for row in countries.row] == ['Germany', 'France']\r
45 \r
46 If you pass the table object along into a template, you can do:\r
47 \r
48     {% for column in countries.columns %}\r
49         {{column}}\r
50     {% endfor %}\r
51 \r
52 Which will give you:\r
53 \r
54     Country Name\r
55     Timezone\r
56 \r
57 Note that ``population`` is skipped (as it has ``visible=False``), that the\r
58 declared verbose name for the ``name`` column is used, and that ``time_zone``\r
59 is converted into a more beautiful string for output automatically.\r
60 \r
61 There are few requirements for the source data of a table. It should be an\r
62 iterable with dict-like objects. Values found in the source data that are\r
63 not associated with a column are ignored, missing values are replaced by\r
64 the column default or None.\r
65 \r
66 Common Workflow\r
67 ~~~~~~~~~~~~~~~\r
68 \r
69 Usually, you are going to use a table like this. Assuming ``CountryTable``\r
70 is defined as above, your view will create an instance and pass it to the\r
71 template:\r
72 \r
73     def list_countries(request):\r
74         data = ...\r
75         countries = CountryTable(data, order_by=request.GET.get('sort'))\r
76         return render_to_response('list.html', {'table': countries})\r
77 \r
78 Note that we are giving the incoming "sort" query string value directly to\r
79 the table, asking for a sort. All invalid column names will (by default) be\r
80 ignored. In this example, only "name" and "tz" are allowed, since:\r
81 \r
82     * "population" has sortable=False\r
83     * "time_zone" has it's name overwritten with "tz".\r
84 \r
85 Then, in the "list.html" template, write:\r
86 \r
87     <table>\r
88     <tr>\r
89         {% for column in table.columns %}\r
90         <th><a href="?sort={{ column.name }}">{{ column }}</a></th>\r
91         {% endfor %}\r
92     </tr>\r
93     {% for row in table.rows %}\r
94         <tr>\r
95         {% for value in row %}\r
96             <td>{{ value }}</td>\r
97         {% endfor %}\r
98         </tr>\r
99     {% endfor %}\r
100     </table>\r
101 \r
102 This will output the data as an HTML table. Note how the table is now fully\r
103 sortable, since our link passes along the column name via the querystring,\r
104 which in turn will be used by the server for ordering. ``order_by`` accepts\r
105 comma-separated strings as input, and "{{ table.order_by }}" will be rendered\r
106 as a such a string.\r
107 \r
108 Instead of the iterator, you can use your knowledge of the table structure to\r
109 access columns directly:\r
110 \r
111     {% if table.columns.tz.visible %}\r
112         {{ table.columns.tz }}\r
113     {% endfor %}\r
114 \r
115 \r
116 Dynamic Data\r
117 ~~~~~~~~~~~~\r
118 \r
119 If any value in the source data is a callable, it will be passed it's own\r
120 row instance and is expected to return the actual value for this particular\r
121 table cell.\r
122 \r
123 Similarily, the colunn default value may also be callable that will takes\r
124 the row instance as an argument (representing the row that the default is\r
125 needed for).\r
126 \r
127 \r
128 Table Options\r
129 -------------\r
130 \r
131 Table-specific options are implemented using the same inner ``Meta`` class\r
132 concept as known from forms and models in Django:\r
133 \r
134     class MyTable(tables.MemoryTable):\r
135         class Meta:\r
136             sortable = True\r
137 \r
138 Currently, for non-model tables, the only supported option is ``sortable``.\r
139 Per default, all columns are sortable, unless a column specifies otherwise.\r
140 This meta option allows you to overwrite the global default for the table.\r
141 \r
142 \r
143 ModelTables\r
144 -----------\r
145 \r
146 Like forms, tables can also be used with models:\r
147 \r
148     class CountryTable(tables.ModelTable):\r
149         id = tables.Column(sortable=False, visible=False)\r
150         class Meta:\r
151             model = Country\r
152             exclude = ['clicks']\r
153 \r
154 The resulting table will have one column for each model field, with the\r
155 exception of "clicks", which is excluded. The column for "id" is overwritten\r
156 to both hide it and deny it sort capability.\r
157 \r
158 When instantiating a ModelTable, you usually pass it a queryset to provide\r
159 the table data:\r
160 \r
161     qs = Country.objects.filter(continent="europe")\r
162     countries = CountryTable(qs)\r
163 \r
164 However, you can also just do:\r
165 \r
166     countries = CountryTable()\r
167 \r
168 and all rows exposed by the default manager of the model the table is based\r
169 on will be used.\r
170 \r
171 If you are using model inheritance, then the following also works:\r
172 \r
173     countries = CountryTable(CountrySubclass)\r
174 \r
175 Note that while you can pass any model, it really only makes sense if the\r
176 model also provides fields for the columns you have defined.\r
177 \r
178 If you just want to use ModelTables, but without auto-generated columns,\r
179 you do not have to list all model fields in the ``exclude`` Meta option.\r
180 Instead, simply don't specify a model.\r
181 \r
182 \r
183 Custom Columns\r
184 ~~~~~~~~~~~~~~\r
185 \r
186 You an add custom columns to your ModelTable that are not based on actual\r
187 model fields:\r
188 \r
189     class CountryTable(tables.ModelTable):\r
190         custom = tables.Column(default="foo")\r
191         class Meta:\r
192             model = Country\r
193 \r
194 Just make sure your model objects do provide an attribute with that name.\r
195 Functions are also supported, so ``Country.custom`` could be a callable.\r
196 \r
197 \r
198 Spanning relationships\r
199 ~~~~~~~~~~~~~~~~~~~~~~\r
200 \r
201 Let's assume you have a ``Country`` model, with a foreignkey ``capital``\r
202 pointing to the ``City`` model. While displaying a list of countries,\r
203 you might want want to link to the capital's geographic location, which is\r
204 stored in ``City.geo`` as a ``(lat, long)`` tuple, on Google Maps.\r
205 \r
206 ModelTables support relationship spanning syntax of Django's database api:\r
207 \r
208     class CountryTable(tables.ModelTable):\r
209         city__geo = tables.Column(name="geo")\r
210 \r
211 This will add a column named "geo", based on the field by the same name\r
212 from the "city" relationship. Note that the name used to define the column\r
213 is what will be used to access the data, while the name-overwrite passed to\r
214 the column constructor just defines a prettier name for us to work with.\r
215 This is to be consistent with auto-generated columns based on model fields,\r
216 where the field/column name naturally equals the source name.\r
217 \r
218 However, to make table defintions more visually appealing and easier to\r
219 read, an alternative syntax is supported: setting the column ``data``\r
220 property to the appropriate string.\r
221 \r
222     class CountryTable(tables.ModelTable):\r
223         geo = tables.Column(data='city__geo')\r
224 \r
225 Note that you don't need to define a relationship's fields as separate\r
226 columns if you already have a column for the relationship itself, i.e.:\r
227 \r
228     class CountryTable(tables.ModelTable):\r
229         city = tables.Column()\r
230 \r
231     for country in countries.rows:\r
232         print country.city.id\r
233         print country.city.geo\r
234         print country.city.founder.name\r
235 \r
236 \r
237 ModelTable Specialities\r
238 ~~~~~~~~~~~~~~~~~~~~~~~\r
239 \r
240 ModelTables currently have some restrictions with respect to ordering:\r
241 \r
242     * Custom columns not based on a model field do not support ordering,\r
243       regardless of the ``sortable`` property (it is ignored).\r
244 \r
245     * A ModelTable column's ``default`` or ``data`` value does not affect\r
246       ordering. This differs from the non-model table behaviour.\r
247 \r
248 If a column is mapped to a method on the model, that method will be called\r
249 without arguments. This behavior differs from non-model tables, where a\r
250 row object will be passed.\r
251 \r
252 If you are using callables (e.g. for the ``default`` or ``data`` column\r
253 options), they will generally be run when a row is accessed, and\r
254 possible repeatetly when accessed more than once. This behavior differs from\r
255 non-model tables, where they would be called once, when the table is\r
256 generated.\r
257 \r
258 Columns\r
259 -------\r
260 \r
261 Columns are what defines a table. Therefore, the way you configure your\r
262 columns determines to a large extend how your table operates.\r
263 \r
264 ``django_tables.columns`` currently defines three classes, ``Column``,\r
265 ``TextColumn`` and ``NumberColumn``. However, the two subclasses currently\r
266 don't do anything special at all, so you can simply use the base class.\r
267 While this will likely change in the future (e.g. when grouping is added),\r
268 the base column class will continue to work by itself.\r
269 \r
270 There are no required arguments. The following is fine:\r
271 \r
272     class MyTable(tables.MemoryTable):\r
273         c = tables.Column()\r
274 \r
275 It will result in a column named "c" in the table. You can specify the\r
276 ``name`` to override this:\r
277 \r
278     c = tables.Column(name="count")\r
279 \r
280 The column is now called and accessed via "count", although the table will\r
281 still use "c" to read it's values from the source. You can however modify\r
282 that as well, by specifying ``data``:\r
283 \r
284     c = tables.Column(name="count", data="count")\r
285 \r
286 For most practicual purposes, "c" is now meaningless. While in most cases\r
287 you will just define your column using the name you want it to have, the\r
288 above is useful when working with columns automatically generated from\r
289 models:\r
290 \r
291     class BookTable(tables.ModelTable):\r
292         book_name = tables.Column(name="name")\r
293         author = tables.Column(data="info__author__name")\r
294         class Meta:\r
295             model = Book\r
296 \r
297 The overwritten ``book_name`` field/column will now be exposed as the\r
298 cleaner "name", and the new "author" column retrieves it's values from\r
299 ``Book.info.author.name``.\r
300 \r
301 Note: ``data`` may also be a callable which will be passed a row object.\r
302 \r
303 Apart from their internal name, you can define a string that will be used\r
304 when for display via ``verbose_name``:\r
305 \r
306     pubdate = tables.Column(verbose_name="Published")\r
307 \r
308 The verbose name will be used, for example, if you put in a template:\r
309 \r
310     {{ column }}\r
311 \r
312 If you don't want a column to be sortable by the user:\r
313 \r
314     pubdate = tables.Column(sortable=False)\r
315 \r
316 Sorting is also affected by ``direction``, which can be used to change the\r
317 *default* sort direction to descending. Note that this option only indirectly\r
318 translates to the actual direction. Normal und reverse order, the terms\r
319 django-tables exposes, now simply mean different things.\r
320 \r
321     pubdate = tables.Column(direction='desc')\r
322 \r
323 If you don't want to expose a column (but still require it to exist, for\r
324 example because it should be sortable nonetheless):\r
325 \r
326     pubdate = tables.Column(visible=False)\r
327 \r
328 The column and it's values will now be skipped when iterating through the\r
329 table, although it can still be accessed manually.\r
330 \r
331 Finally, you can specify default values for your columns:\r
332 \r
333     health_points = tables.Column(default=100)\r
334 \r
335 Note that how the default is used and when it is applied differs between\r
336 static and ModelTables.\r
337 \r
338 \r
339 The table.columns container\r
340 ---------------------------\r
341 \r
342 While you can iterate through ``columns`` and get all the currently visible\r
343 columns, it further provides features that go beyond a simple iterator.\r
344 \r
345 You can access all columns, regardless of their visibility, through\r
346 ``columns.all``.\r
347 \r
348 ``columns.sortable`` is a handy shortcut that exposes all columns which's\r
349 ``sortable`` attribute is True. This can be very useful in templates, when\r
350 doing {% if column.sortable %} can conflict with {{ forloop.last }}.\r
351 \r
352 \r
353 Tables and Pagination\r
354 ---------------------\r
355 \r
356 If your table has a large number of rows, you probably want to paginate\r
357 the output. There are two distinct approaches.\r
358 \r
359 First, you can just paginate over ``rows`` as you would do with any other\r
360 data:\r
361 \r
362     table = MyTable(queryset)\r
363     paginator = Paginator(table.rows, 10)\r
364     page = paginator.page(1)\r
365 \r
366 You're not necessarily restricted to Django's own paginator (or subclasses) -\r
367 any paginator should work with this approach, so long it only requires\r
368 ``rows`` to implement ``len()``, slicing, and, in the case of ModelTables, a\r
369 ``count()`` method. The latter means that the ``QuerySetPaginator`` also\r
370 works as expected.\r
371 \r
372 Alternatively, you may use the ``paginate`` feature:\r
373 \r
374     table = MyTable(queryset)\r
375     table.paginate(Paginator, 10, page=1, orphans=2)\r
376     for row in table.rows.page():\r
377         pass\r
378     table.paginator                # new attributes\r
379     table.page\r
380 \r
381 The table will automatically create an instance of ``QuerySetPaginator``,\r
382 passing it's own data as the first argument and additionally any arguments\r
383 you have specified, except for ``page``. You may use any paginator, as long\r
384 as it follows the Django protocol:\r
385 \r
386     * Take data as first argument.\r
387     * Support a page() method returning an object with an ``object_list``\r
388       attribute, exposing the paginated data.\r
389 \r
390 Note that due to the abstraction layer that django-tables represents, it is\r
391 not necessary to use Django's QuerySetPaginator with ModelTables. Since the\r
392 table knows that it holds a queryset, it will automatically choose to use\r
393 count() to determine the data length (which is exactly what\r
394 QuerySetPaginator would do).\r
395 \r
396 Ordering\r
397 --------\r
398 \r
399 The syntax is similar to that of the Django database API. Order may be\r
400 specified a list (or tuple) of column names. If prefixed with a hyphen, the\r
401 ordering for that particular column will be in reverse order.\r
402 \r
403 Random ordering is currently not supported.\r
404 \r
405 Interacting with order\r
406 ~~~~~~~~~~~~~~~~~~~~~~\r
407 \r
408 Letting the user change the order of a table is a common scenario. With\r
409 respect to Django, this means adding links to your table output that will\r
410 send off the appropriate arguments to the server. django-tables attempts\r
411 to help with you that.\r
412 \r
413 A bound column, that is a colum accessed through a table instance, provides\r
414 the following attributes:\r
415 \r
416     - ``name_reversed`` will simply return the column name prefixed with a\r
417       hyphen; this is useful in templates, where string concatenation can\r
418       at times be difficult.\r
419 \r
420     - ``name_toggled`` checks the tables current order, and will then\r
421     return the column either prefixed with an hyphen (for reverse ordering)\r
422     or without, giving you the exact opposite order. If the column is\r
423     currently not ordered, it will start off in non-reversed order.\r
424 \r
425 It is easy to be confused about the difference between the ``reverse`` and\r
426 ``toggle`` terminology. django-tables tries to put normal/reverse-order\r
427 abstraction on top of "ascending/descending", where as normal order could\r
428 potentially mean either ascending or descending, depending on the column.\r
429 \r
430 Commonly, you see tables that indicate what columns they are currently\r
431 ordered by using little arrows. To implement this:\r
432 \r
433     - ``is_ordered``: Returns True if the column is in the current\r
434     ``order_by``, regardless of the polarity.\r
435 \r
436     - ``is_ordered_reverse``, ``is_ordered_straight``: Returns True if the\r
437     column is ordered in reverse or non-reverse, respectively, otherwise\r
438     False.\r
439 \r
440 The above is usually enough for most simple cases, where tables are only\r
441 ordered by a single column. For scenarios in which multi-column order is\r
442 used, additional attributes are available:\r
443 \r
444     - ``order_by``: Return the current order, but with the current column\r
445     set to normal ordering. If the current column is not already part of\r
446     the order, it is appended. Any existing columns in the order are\r
447     maintained as-is.\r
448 \r
449     - ``order_by_reversed``, ``order_by_toggled``: Similarly, return the\r
450     table's current ``order_by`` with the column set to reversed or toggled,\r
451     respectively. Again, it is appended if not already ordered.\r
452 \r
453 Additionally, ``table.order_by.toggle()`` may also be useful in some cases:\r
454 It will toggle all order columns and should thus give you the exact\r
455 opposite order.\r
456 \r
457 The following is a simple example of single-column ordering. It shows a list\r
458 of sortable columns, each clickable, and an up/down arrow next to the one\r
459 that is currently used to sort the table.\r
460 \r
461     Sort by:\r
462     {% for column in table.columns %}\r
463         {% if column.sortable %}\r
464             <a href="?sort={{ column.name_toggled }}">{{ column }}</a>\r
465             {% if column.is_ordered_straight %}<img src="down.png" />{% endif %}\r
466             {% if column.is_ordered_reverse %}<img src="up.png" />{% endif %}\r
467         {% endif %}\r
468     {% endfor %}\r
469 \r
470 \r
471 Error handling\r
472 --------------\r
473 \r
474 Passing incoming query string values from the request directly to the\r
475 table constructor is a common thing to do. However, such data can easily\r
476 be invalid, be it that a user manually modified it, or someone put up a\r
477 broken link. In those cases, you usually would not want to raise an\r
478 exception (nor be notified by Django's error notification mechanism) -\r
479 there is nothing you could do anyway.\r
480 \r
481 Because of this, such errors will by default be silently ignored. For\r
482 example, if one out of three columns in an "order_by" is invalid, the other\r
483 two will still be applied:\r
484 \r
485     table.order_by = ('name', 'totallynotacolumn', '-date)\r
486     assert table.order_by = ('name', '-date)\r
487 \r
488 This ensures that the following table will be created regardless of the\r
489 string in "sort.\r
490 \r
491     table = MyTable(data, order_by=request.GET.get('sort'))\r
492 \r
493 However, if you want, you can disable this behaviour and have an exception\r
494 raised instead, using:\r
495 \r
496     import django_tables\r
497     django_tables.options.IGNORE_INVALID_OPTIONS = False\r
498 \r
499 \r
500 Template Utilities\r
501 ------------------\r
502 \r
503 If you want the give your users the ability to interact with your table (e.g.\r
504 change the ordering), you will need to create urls with the appropriate\r
505 queries. To simplify that process, django-tables comes with a helpful\r
506 templatetag:\r
507 \r
508     {% set_url_param sort="name" %}       # ?sort=name\r
509     {% set_url_param sort="" %}           # delete "sort" param\r
510 \r
511 The template library can be found in 'django_modules.app.templates.tables'.\r
512 If you add ''django_modules.app' to your INSTALLED_APPS setting, you will\r
513 be able to do:\r
514 \r
515     {% load tables %}\r
516 \r
517 Note: The tag requires the current request to be available as ``request``\r
518 in the context (usually, this means activating the Django request context\r
519 processor).\r
520 \r