django-tables: A QuerySet renderer

While trying to add a simple sorting feature to the critify game listing, I went off on a strange train of thought involving a bunch of future functionality I only have a very vague picture of, and decided that it would be best to choose the most complex approach possible and create a separate, overarchitected abstraction layer for that very purpose.

Well, something like that, though hopefully not quite as bad. Basically, the idea is:

  • to making rendering tabular data a little easier by encapsulating some repetitive parts.
  • make working with tabular data a lot easier when it comes to the user interacting with the table.

You would define a table, which is sort of a cross between a model and a form, define it’s columns (i.e. fields), and then tell it how you want to sort, filter and group the data.

Some snippets of actual code should probably explain it best.

First, let’s define a table:

import django_tables as tables

class BookTable(tables.ModelTable):
    id = tables.Column(sortable=False, visible=False)
    book_name = tables.Column(name='title')
    author = tables.Column(data='author__name')
    class Meta:
        model = Book

The table is based on the Book model; thus, it will have a column for each model field, and in addition the locally defined columns will override the default ones, or add to them, very much like newforms works (you’ll find that even the internals are at times very similar to the newforms code).

So, now that we have defined the table, let’s create an instance:

initial_queryset = Book.objects.all()
books = BookTable(
         initial_queryset,
         order_by=request.GET.get('sort', 'title'))

We tell the table to operate on the full book data set, and to order it by whatever the user sends along via the query string, or fall back to the default sort order based on the title (book_name) column.

Finally, you would send the table to the template:

return render_to_response('table.html', {'table': books})

Where it is easy to print it out:

<table>

<tr>
  {% for column in table.columns %}
  <th>
    {% if column.sortable %}
      <a href="?sort={{ column.name_toggled }}">
        {{ column }}
      </a>
      {% if column.is_ordered_reverse %}
        <img src="up.png" />
      {% else %}
        <img src="down.png" />
      {% endif %}
    {% else %}
      {{ column }}
    {% endif %}
  </th>
  {% endfor %}
</tr>

{% for row in table.rows %}
  <tr>
  {% for value in row %}
    <td>{{ value }}<td>
  {% endfor %}
  </tr>
{% endfor %}

</table>

The above template code generically renders any table you give it, restricted to it’s visible columns, and allows each column to be sorted in ascending or descending order (so long sorting is not disabled for a column). It gives you those nice arrow icons too.

At this point you will probably wonder if it’s not a lot simpler to just say:

Books.objects.all().order_by(request.GET.get('sort'))

And you’d be right, it is, and I indicated as much at the beginning of the post. There are however a couple of nice things about the table abstraction:

  • While Django’s ORM already protects you against SQL injections, you still might like to play it safe and limit the possible values of the sort parameter (which will also ensure users won’t be able to guess your database schema by trying different values). Using django-tables, this is built in.
  • It easily allows you to expose your fields under a different name, e.g. date instead of published_at. True, it’s just cosmetics, but personally I am (probably unhealthily) particular about stuff like that.
  • Both previous points are especially relevant when you want to order via a relationship, e.g. author__birthdate. The double underscore doesn’t look all that nice, gives a rather clear insight into your database layout and also exposes that you are using Django, which may be undesirable.
  • It easily allows you to move control over what fields to expose from your templates into python. Your templates will need to deal to a lesser extend with what to render, but rather on how to render it.
  • Boilerplate code required to let the user sort the table, particularly when it comes to allowing toggling between descending/ascending, or even multiple sort fields, is reduced.

Additionally, it might be worth noting that there is a non-model implementation as well (use Table instead of ModelTable), that bascially does the same thing with static python data, e.g. a list of dicts.

To be perfectly honest, at this point I have no idea myself if much of this makes any sense or how useful it actually is. The fact that I haven’t really thought that much beyond the (already implemented) sorting functionality is not helping either (i.e. grouping, filtering…). I am going to toy with it for a bit though, and you are invited to do so too.

The project is maintained in bazaar and can be retrieved via Launchpad:

bzr branch lp:django-tables

There is also a source code browser, where you’ll also find the readme with a lot more information.

For questions/discussions/support use the django-apps Google Group and prefix your message with (tables).

15 thoughts on “django-tables: A QuerySet renderer

  1. Great article! Two typos:

    return render_to_response(‘table.html’, {‘books’: books}) should be
    return render_to_response(‘table.html’, {‘table’: books})

    and

    And you’d be write, it is, should be
    And you’d be right, it is,

    Keep up the great blog!

    Like

  2. Metin, thanks, both fixed. I hate it when I make the latter type of mistake (also happens a lot with now/know).

    Like

  3. django-table is great project! There should be such project in django.contrib.

    Can you please show an example of using paginator ?

    I’ve read README few times but can’t get this working.

    Thanks

    Like

  4. Robert, what part about the example in the README do you have trouble with? Calling, for example, table.paginate(), and then using iterating over the rows in “table.rows.page” should work as advertised.

    I suggest you take further questions to the django-apps Google Group, which should work much better than this blog.

    http://groups.google.com/group/django-apps

    Like

  5. The djang-tables is great project. I belive, django.contrib will be right place for it. I have just subscribed to its LP trunk branch, downloaded it and started playing – it looks like a *very* useful tool.

    Thanks!

    Like

  6. Thank you for the code. You have a good idea.

    I got the package from pypi today, and I have few issues with it. Is that the right place to get your latest version? Where can I find additional documentation?

    1) Every table column is followed by an empty column. I was able to counter this behaviour by inserting empty TH after each TH with content. Problem seems to be in ModelTable.
    2) ModelTable.__init__ doesn’t accept the sort parameter.
    3) How to use is_ordered_reverse (should be automatic after item 2 is fixed)?

    I agree that your code should be included in contrib, although after some cleanup and documentation.

    Like

  7. Erkki,

    the latest code is on Launchpad and the Bazaar repository linked to in the post. The current documentation is limited to the README file that is part of the distribution.

    Generally, for questions please use the django-apps Group and prefix your message with “(tables)”:

    http://groups.google.com/group/django-apps

    As for your questions:

    1) I’d need some code to reproduce this. Feel free to use django-apps, post a bug report on Launchpad, or send me an email (michael@elsdoerfer.info).

    2) The parameter is actually called “order_by” – sorry, a mistake I made in this post here (now fixed). The README should be correct.

    3) Sorry, I’m not quite sure what you mean here.

    Like

  8. Thank you for the information. I found that the reason for 1) was the missing in your example (change the latter to ). In 3) I’m referring to the mechanism which tells the template if the data is currently reverse ordered by this column.

    Like

  9. I had a thought on the sorting of the custom columns. If, instead of name, it would be possible to specify column_heading and db_sort_field, you could fashion custom formatted but still sortable columns.

    I can accomplish this now with a db column, which contains 0 or 1, which gets custom formatted to “yes” or “no”, but I have to fix the column heading to the db column name.

    Like

  10. Erkki,

    you should be able to use:

    – “verbose_name” for the column header text.
    – “name” – for the field name.
    – “data” – specify a callable for custom formatting.

    In other words, something along those lines might work:

        def format(row):
            if row['name_to_expose_as'] == 0: return "yes"
            else: return "no"
        name_of_field_in_model = tables.Column(name="name_to_expose_as", verbose_name="column header text", data=format)
    

    I agree that how the different options interact is somewhat confusing right now, but I’m not yet quite sure what the right way would be to make it more straightforward. I’m reluctant to introduce many more arguments to Column(), since there are already so many, and I suspect that is to some extend where the confusion stems from.

    Apart from that, I’d really prefer to use the mailinglist for future discussions.

    Like

  11. Hey! Trying to get this to work with Django 1.1 and I get:

    Exception Value:
    ‘WSSWSite’ object has no attribute ‘get’
    Exception Location: /Users/ssteiner/.virtualenvs/wssw1/lib/python2.6/site-packages/django_tables/tables.py in _build_snapshot, line 261

    It’s pretty much straight out of the docs, just with my own classes.

    Any idea what’s going on?

    I searched through the code but get() is not explicitly called anywhere in django-tables anywhere so I’m a little lost…

    Thanks,

    S

    Like

    1. It’s called in the given location on the row objects you are passing in – they originally where intended to be dicts, although I guess it would be preferrable for django-tables to use indexing. I can’t tell if that would help in your case though, or whether you just have a bug in your code. What is “WSSWSite”?

      I’d prefer you use the django-apps Google Group for support requests though.

      Like

  12. I kept getting every other cell as an empty cell. After spending ages looking through the code the problem is actually more obvious. It’s in the template example above. There is a slash missing.

    Should be:

    {{ value }}

    not

    {{ value }}

    Like

Leave a reply to Erkki Tapola Cancel reply