django-tables ============= A Django QuerySet renderer. Installation ------------ Adding django-tables to your INSTALLED_APPS settings is optional, it'll get you the ability to load some template utilities via {% load tables %}, but apart from that, ``import django_tables as tables`` should get you going. Running the test suite ---------------------- The test suite uses nose: http://somethingaboutorange.com/mrl/projects/nose/ Working with Tables ------------------- A table class looks very much like a form: import django_tables as tables class CountryTable(tables.MemoryTable): name = tables.Column(verbose_name="Country Name") population = tables.Column(sortable=False, visible=False) time_zone = tables.Column(name="tz", default="UTC+1") Instead of fields, you declare a column for every piece of data you want to expose to the user. To use the table, create an instance: countries = CountryTable([{'name': 'Germany', population: 80}, {'name': 'France', population: 64}]) Decide how the table should be sorted: countries.order_by = ('name',) assert [row.name for row in countries.row] == ['France', 'Germany'] countries.order_by = ('-population',) assert [row.name for row in countries.row] == ['Germany', 'France'] If you pass the table object along into a template, you can do: {% for column in countries.columns %} {{column}} {% endfor %} Which will give you: Country Name Timezone Note that ``population`` is skipped (as it has ``visible=False``), that the declared verbose name for the ``name`` column is used, and that ``time_zone`` is converted into a more beautiful string for output automatically. There are few requirements for the source data of a table. It should be an iterable with dict-like objects. Values found in the source data that are not associated with a column are ignored, missing values are replaced by the column default or None. Common Workflow ~~~~~~~~~~~~~~~ Usually, you are going to use a table like this. Assuming ``CountryTable`` is defined as above, your view will create an instance and pass it to the template: def list_countries(request): data = ... countries = CountryTable(data, order_by=request.GET.get('sort')) return render_to_response('list.html', {'table': countries}) Note that we are giving the incoming "sort" query string value directly to the table, asking for a sort. All invalid column names will (by default) be ignored. In this example, only "name" and "tz" are allowed, since: * "population" has sortable=False * "time_zone" has it's name overwritten with "tz". Then, in the "list.html" template, write: {% for column in table.columns %} {% endfor %} {% for row in table.rows %} {% for value in row %} {% endfor %} {% endfor %}
{{ column }}
{{ value }}
This will output the data as an HTML table. Note how the table is now fully sortable, since our link passes along the column name via the querystring, which in turn will be used by the server for ordering. ``order_by`` accepts comma-separated strings as input, and "{{ table.order_by }}" will be rendered as a such a string. Instead of the iterator, you can use your knowledge of the table structure to access columns directly: {% if table.columns.tz.visible %} {{ table.columns.tz }} {% endfor %} Dynamic Data ~~~~~~~~~~~~ If any value in the source data is a callable, it will be passed it's own row instance and is expected to return the actual value for this particular table cell. Similarily, the colunn default value may also be callable that will takes the row instance as an argument (representing the row that the default is needed for). Table Options ------------- Table-specific options are implemented using the same inner ``Meta`` class concept as known from forms and models in Django: class MyTable(tables.MemoryTable): class Meta: sortable = True Currently, for non-model tables, the only supported option is ``sortable``. Per default, all columns are sortable, unless a column specifies otherwise. This meta option allows you to overwrite the global default for the table. ModelTables ----------- Like forms, tables can also be used with models: class CountryTable(tables.ModelTable): id = tables.Column(sortable=False, visible=False) class Meta: model = Country exclude = ['clicks'] The resulting table will have one column for each model field, with the exception of "clicks", which is excluded. The column for "id" is overwritten to both hide it and deny it sort capability. When instantiating a ModelTable, you usually pass it a queryset to provide the table data: qs = Country.objects.filter(continent="europe") countries = CountryTable(qs) However, you can also just do: countries = CountryTable() and all rows exposed by the default manager of the model the table is based on will be used. If you are using model inheritance, then the following also works: countries = CountryTable(CountrySubclass) Note that while you can pass any model, it really only makes sense if the model also provides fields for the columns you have defined. If you just want to use ModelTables, but without auto-generated columns, you do not have to list all model fields in the ``exclude`` Meta option. Instead, simply don't specify a model. Custom Columns ~~~~~~~~~~~~~~ You an add custom columns to your ModelTable that are not based on actual model fields: class CountryTable(tables.ModelTable): custom = tables.Column(default="foo") class Meta: model = Country Just make sure your model objects do provide an attribute with that name. Functions are also supported, so ``Country.custom`` could be a callable. Spanning relationships ~~~~~~~~~~~~~~~~~~~~~~ Let's assume you have a ``Country`` model, with a foreignkey ``capital`` pointing to the ``City`` model. While displaying a list of countries, you might want want to link to the capital's geographic location, which is stored in ``City.geo`` as a ``(lat, long)`` tuple, on Google Maps. ModelTables support relationship spanning syntax of Django's database api: class CountryTable(tables.ModelTable): city__geo = tables.Column(name="geo") This will add a column named "geo", based on the field by the same name from the "city" relationship. Note that the name used to define the column is what will be used to access the data, while the name-overwrite passed to the column constructor just defines a prettier name for us to work with. This is to be consistent with auto-generated columns based on model fields, where the field/column name naturally equals the source name. However, to make table defintions more visually appealing and easier to read, an alternative syntax is supported: setting the column ``data`` property to the appropriate string. class CountryTable(tables.ModelTable): geo = tables.Column(data='city__geo') Note that you don't need to define a relationship's fields as separate columns if you already have a column for the relationship itself, i.e.: class CountryTable(tables.ModelTable): city = tables.Column() for country in countries.rows: print country.city.id print country.city.geo print country.city.founder.name ModelTable Specialities ~~~~~~~~~~~~~~~~~~~~~~~ ModelTables currently have some restrictions with respect to ordering: * Custom columns not based on a model field do not support ordering, regardless of the ``sortable`` property (it is ignored). * A ModelTable column's ``default`` or ``data`` value does not affect ordering. This differs from the non-model table behaviour. If a column is mapped to a method on the model, that method will be called without arguments. This behavior differs from non-model tables, where a row object will be passed. If you are using callables (e.g. for the ``default`` or ``data`` column options), they will generally be run when a row is accessed, and possible repeatetly when accessed more than once. This behavior differs from non-model tables, where they would be called once, when the table is generated. Columns ------- Columns are what defines a table. Therefore, the way you configure your columns determines to a large extend how your table operates. ``django_tables.columns`` currently defines three classes, ``Column``, ``TextColumn`` and ``NumberColumn``. However, the two subclasses currently don't do anything special at all, so you can simply use the base class. While this will likely change in the future (e.g. when grouping is added), the base column class will continue to work by itself. There are no required arguments. The following is fine: class MyTable(tables.MemoryTable): c = tables.Column() It will result in a column named "c" in the table. You can specify the ``name`` to override this: c = tables.Column(name="count") The column is now called and accessed via "count", although the table will still use "c" to read it's values from the source. You can however modify that as well, by specifying ``data``: c = tables.Column(name="count", data="count") For most practicual purposes, "c" is now meaningless. While in most cases you will just define your column using the name you want it to have, the above is useful when working with columns automatically generated from models: class BookTable(tables.ModelTable): book_name = tables.Column(name="name") author = tables.Column(data="info__author__name") class Meta: model = Book The overwritten ``book_name`` field/column will now be exposed as the cleaner "name", and the new "author" column retrieves it's values from ``Book.info.author.name``. Note: ``data`` may also be a callable which will be passed a row object. Apart from their internal name, you can define a string that will be used when for display via ``verbose_name``: pubdate = tables.Column(verbose_name="Published") The verbose name will be used, for example, if you put in a template: {{ column }} If you don't want a column to be sortable by the user: pubdate = tables.Column(sortable=False) Sorting is also affected by ``direction``, which can be used to change the *default* sort direction to descending. Note that this option only indirectly translates to the actual direction. Normal und reverse order, the terms django-tables exposes, now simply mean different things. pubdate = tables.Column(direction='desc') If you don't want to expose a column (but still require it to exist, for example because it should be sortable nonetheless): pubdate = tables.Column(visible=False) The column and it's values will now be skipped when iterating through the table, although it can still be accessed manually. Finally, you can specify default values for your columns: health_points = tables.Column(default=100) Note that how the default is used and when it is applied differs between static and ModelTables. The table.columns container --------------------------- While you can iterate through ``columns`` and get all the currently visible columns, it further provides features that go beyond a simple iterator. You can access all columns, regardless of their visibility, through ``columns.all``. ``columns.sortable`` is a handy shortcut that exposes all columns which's ``sortable`` attribute is True. This can be very useful in templates, when doing {% if column.sortable %} can conflict with {{ forloop.last }}. Tables and Pagination --------------------- If your table has a large number of rows, you probably want to paginate the output. There are two distinct approaches. First, you can just paginate over ``rows`` as you would do with any other data: table = MyTable(queryset) paginator = Paginator(table.rows, 10) page = paginator.page(1) You're not necessarily restricted to Django's own paginator (or subclasses) - any paginator should work with this approach, so long it only requires ``rows`` to implement ``len()``, slicing, and, in the case of ModelTables, a ``count()`` method. The latter means that the ``QuerySetPaginator`` also works as expected. Alternatively, you may use the ``paginate`` feature: table = MyTable(queryset) table.paginate(Paginator, 10, page=1, orphans=2) for row in table.rows.page(): pass table.paginator # new attributes table.page The table will automatically create an instance of ``QuerySetPaginator``, passing it's own data as the first argument and additionally any arguments you have specified, except for ``page``. You may use any paginator, as long as it follows the Django protocol: * Take data as first argument. * Support a page() method returning an object with an ``object_list`` attribute, exposing the paginated data. Note that due to the abstraction layer that django-tables represents, it is not necessary to use Django's QuerySetPaginator with ModelTables. Since the table knows that it holds a queryset, it will automatically choose to use count() to determine the data length (which is exactly what QuerySetPaginator would do). Ordering -------- The syntax is similar to that of the Django database API. Order may be specified a list (or tuple) of column names. If prefixed with a hyphen, the ordering for that particular column will be in reverse order. Random ordering is currently not supported. Interacting with order ~~~~~~~~~~~~~~~~~~~~~~ Letting the user change the order of a table is a common scenario. With respect to Django, this means adding links to your table output that will send off the appropriate arguments to the server. django-tables attempts to help with you that. A bound column, that is a colum accessed through a table instance, provides the following attributes: - ``name_reversed`` will simply return the column name prefixed with a hyphen; this is useful in templates, where string concatenation can at times be difficult. - ``name_toggled`` checks the tables current order, and will then return the column either prefixed with an hyphen (for reverse ordering) or without, giving you the exact opposite order. If the column is currently not ordered, it will start off in non-reversed order. It is easy to be confused about the difference between the ``reverse`` and ``toggle`` terminology. django-tables tries to put normal/reverse-order abstraction on top of "ascending/descending", where as normal order could potentially mean either ascending or descending, depending on the column. Commonly, you see tables that indicate what columns they are currently ordered by using little arrows. To implement this: - ``is_ordered``: Returns True if the column is in the current ``order_by``, regardless of the polarity. - ``is_ordered_reverse``, ``is_ordered_straight``: Returns True if the column is ordered in reverse or non-reverse, respectively, otherwise False. The above is usually enough for most simple cases, where tables are only ordered by a single column. For scenarios in which multi-column order is used, additional attributes are available: - ``order_by``: Return the current order, but with the current column set to normal ordering. If the current column is not already part of the order, it is appended. Any existing columns in the order are maintained as-is. - ``order_by_reversed``, ``order_by_toggled``: Similarly, return the table's current ``order_by`` with the column set to reversed or toggled, respectively. Again, it is appended if not already ordered. Additionally, ``table.order_by.toggle()`` may also be useful in some cases: It will toggle all order columns and should thus give you the exact opposite order. The following is a simple example of single-column ordering. It shows a list of sortable columns, each clickable, and an up/down arrow next to the one that is currently used to sort the table. Sort by: {% for column in table.columns %} {% if column.sortable %} {{ column }} {% if column.is_ordered_straight %}{% endif %} {% if column.is_ordered_reverse %}{% endif %} {% endif %} {% endfor %} Error handling -------------- Passing incoming query string values from the request directly to the table constructor is a common thing to do. However, such data can easily be invalid, be it that a user manually modified it, or someone put up a broken link. In those cases, you usually would not want to raise an exception (nor be notified by Django's error notification mechanism) - there is nothing you could do anyway. Because of this, such errors will by default be silently ignored. For example, if one out of three columns in an "order_by" is invalid, the other two will still be applied: table.order_by = ('name', 'totallynotacolumn', '-date) assert table.order_by = ('name', '-date) This ensures that the following table will be created regardless of the string in "sort. table = MyTable(data, order_by=request.GET.get('sort')) However, if you want, you can disable this behaviour and have an exception raised instead, using: import django_tables django_tables.options.IGNORE_INVALID_OPTIONS = False Template Utilities ------------------ If you want the give your users the ability to interact with your table (e.g. change the ordering), you will need to create urls with the appropriate queries. To simplify that process, django-tables comes with a helpful templatetag: {% set_url_param sort="name" %} # ?sort=name {% set_url_param sort="" %} # delete "sort" param The template library can be found in 'django_modules.app.templates.tables'. If you add ''django_modules.app' to your INSTALLED_APPS setting, you will be able to do: {% load tables %} Note: The tag requires the current request to be available as ``request`` in the context (usually, this means activating the Django request context processor).