Skip to content

jqb/django-tables

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.Table):
        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:

    <table>
    <tr>
        {% for column in table.columns %}
        <th><a href="?sort={{ column.name }}">{{ column }}</a></th>
        {% endfor %}
    </tr>
    {% for row in table.rows %}
        <tr>
        {% for value in row %}
            <td>{{ value }}</td>
        {% endfor %}
        </tr>
    {% endfor %}
    </table>

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.Table):
        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.Table):
        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 %}
            <a href="?sort={{ column.name_toggled }}">{{ column }}</a>
            {% if column.is_ordered_straight %}<img src="down.png" />{% endif %}
            {% if column.is_ordered_reverse %}<img src="up.png" />{% 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).


TODO
----
    - "data", if used to format for display, affect sorting; this stuff needs
      some serious redesign.
    - as_html methods are all empty right now
    - table.column[].values is a stub
    - filters
    - grouping
    - choices support for columns (internal column value will be looked up
      for output)
    - for columns that span model relationships, automatically generate
      select_related(); this is important, since right now such an e.g.
      order_by would cause rows to be dropped (inner join).
    - initialize auto-generated columns with the relevant properties of the
      model fields (verbose_name, editable=visible?, ...)
    - remove support for callable fields? this has become obsolete since we
      Column.data property; also, it's easy to make the call manually, or let
      the template engine handle it
    - tests could use some refactoring, they are currently all over the place
    - what happens if duplicate column names are used? we currently don't
      check for that at all

Filters
~~~~~~~

Filtering is already easy (just use the normal queryset methods), but
filter support in django-tables would want to hide the Django ORM syntax
from the user.

    * For example, say a ``models.DateTimeField`` should be filtered
      by year: the user would only see ``date=2008`` rather than maybe
      ``published_at__year=2008``.

    * Say you want to filter out ``UserProfile`` rows that do not have
      an avatar image set. The user would only see ```no_avatar``, which
      in Django ORM syntax might map to
      ``Q(avatar__isnull=True) | Q(avatar='')``.

Filters would probably always belong to a column, and be defined along
side one.

    class BookTable(tables.ModelTable):
        date = tables.Column(filter='published_at__year')

If a filter is needed that does not belong to a single colunn, a column
would have to be defined for just that filter. A ``tables.Filter`` object
could be provided that would basically be a column, but with default
properties set so that the column functionality is disabled as far as
possible (i.e. ``visible=False`` etc):

    class BookTable(tables.ModelTable):
        date = tables.Column(filter='published_at__year')
        has_cover = tables.Filter('cover__isnull', value=True)

Or, if Filter() gets a lot of additional functionality like ``value``,
we could generally make it available to all filters like so:

    class BookTable(tables.ModelTable):
        date = tables.Column(filter=tables.Filter('published_at__year', default=2008))
        has_cover = tables.Filter('cover__isnull', value=True)

More complex filters should be supported to (e.g. combine multiple Q
objects, support ``exclude`` as well as ``filter``). Allowing the user
to somehow specify a callable probably is the easiest way to enable this.

The filter querystring syntax, as exposed to the user, could look like this:

    /view/?filter=name:value
    /view/?filter=name

It would also be cool if filters could be combined. However, in that case
it would also make sense to make it possible to choose individual filters
which cannot be combined with any others, or maybe even allow the user
to specify complex dependencies. That may be pushing it though, and anyway
won't make it into the first version.

    /view/?filter=name:value,foo:bar

We need to think about how we make the functionality available to
templates.

Another feature that would be important is the ability to limit the valid
values for a filter, e.g. in the date example only years from 2000 to 2008.

Use django-filters:
    - would support html output
    - would not work at all with our planned QueryTable
    - conflicts somewhat in that it also allows ordering
    
To autoamtically allow filtering a column with filter=True, we would need to
have subclasses for each model class, even if it just redirects to use the 
correct filter class; 

If not using django-filter, we wouldn't have different filter types; filters 
would just hold the data, and each column would know how to apply it.

About

django-tables fork

Basically created to add callbacks.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages