Using django-tables2, django-filter and django-crispy-forms together

Using django-tables2, django-filter and django-crispy-forms together

I was recently working on a very CRUDy prototype and decided to use some Django applications and tools together I hadn't combined yet:

A view that uses all three apps together could look like this:

class FooTableView(TemplateView):
    template_name = 'app/foo_table.html'

    def get_queryset(self, **kwargs):
        return Foo.objects.all()

    def get_context_data(self, **kwargs):
        context = super(FooTableView, self).get_context_data(**kwargs)
        filter = FooFilter(self.request.GET, queryset=self.get_queryset(**kwargs))
        filter.form.helper = FooFilterFormHelper()
        table = FooTable(filter.qs)
        RequestConfig(self.request).configure(table)
        context['filter'] = filter
        context['table'] = table
        return context

While this is a basic example there's still a lot going on. The get_context_data() method gets called automatically by the TemplateView and populates the template context with the filter and table objects.

At first the code creates an instance of FooFilter and passes it the request's GET data, and the queryset to work on. The filter does what you'd expect and filters the queryset.

The filter object also includes a form for users to filter the data. A crispy form helper is added to style and configure the form.

At last the table object gets created and configured, based on the filtered queryset and the request data.

Displaying everything on the frontend now becomes as easy as:

{% load django_tables2 crispy_forms_tags %}

{% crispy filter.form filter.form.helper %}
{% render_table table %}

This code should be enough to get you started, assuming that you read the app-specific documentation. Styling everything of course depends on other things in your project, and your preferences.

Pagination and the SingleTableView

The view above is nice, but if you have to paginate your table you might be interested in using a ListView or the SingleTableView that comes with django-tables2.

If you want to use several views like this in your application you would also benefit from using a generic view that takes additional parameters like filter_class and formhelper_class.

Here is an example for such a view based on the SingleTableView. It's not the most robust code, the configuration for example could be optional, get_queryset() can't be modified easily, etc. But if you're still reading you can probably fix whatever issues you find yourself :-)

from django_tables2 import SingleTableView

class PagedFilteredTableView(SingleTableView):
    filter_class = None
    formhelper_class = None
    context_filter_name = 'filter'

    def get_queryset(self, **kwargs):
        qs = super(PagedFilteredTableView, self).get_queryset()
        self.filter = self.filter_class(self.request.GET, queryset=qs)
        self.filter.form.helper = self.formhelper_class()
        return self.filter.qs

    def get_table(self, **kwargs):
        table = super(PagedFilteredTableView, self).get_table()
        RequestConfig(self.request, paginate={'page': self.kwargs['page'],
                            "per_page": self.paginate_by}).configure(table)
        return table

    def get_context_data(self, **kwargs):
        context = super(PagedFilteredTableView, self).get_context_data()
        context[self.context_filter_name] = self.filter
        return context

One thing that changed changed is the RequestConfig line that configures the table's pagination.

As SingleTableView inherits from Django's ListView you automatically get a Paginator object in the template context as paginator.

The get_queryset() method was changed to apply the filter and to return the filtered queryset. That filtered data ends up in the table in get_table() and gets paged. Aftert that it is added to the template context together with the filter.

With this generic view adding more paged filtered crispy table views takes just a few lines of code:

class FooTableView(PagedFilteredTableView):
    model = Foo
    table_class = FooTable
    template_name = 'app/foo_table.html'
    paginate_by = 50
    filter_class = FooFilter
    formhelper_class = FooFilterFormHelper

Published on March 18, 2014 at 7:40 p.m. by Nicolas and tagged development, Django. You can follow the discussion with the comment feed for this post. Feeling generous? Donate!

8 comments

  1. avatar
    wrote this comment on
    I think this is an excellent intro, but your example leaves out how you styled the table to get the filter to display on top of it :(
    Reply to this comment
    1. avatar
      wrote this comment on
      The filter is displayed with the {% crispy %} template tag, that's all there is to it.
      Reply to this comment
  2. avatar
    wrote this comment on
    I'm having trouble filling in the missing pieces for the second example, the PagedFilteredTableView. The PagedFilteredTableView itself is meant to be a generic class, right, and then the actual data you want to see is another class, subclassed from that? Then where do the classes FooFilter and FooFilterFormHelper come from?
    Reply to this comment
    1. avatar
      wrote this comment on
      Right, PagedFilteredTableView is a generic view. The FooFilter and FooFilterFormHelper are django_filters.FilterSet and crispy_forms.FormHelper instances you have to write.

      You probably want to work with django-filters and crispy-forms separately before trying to integrate it all.

      I'll see if I can make the example a little more verbose, thank you and good luck!
      Reply to this comment
      1. avatar
        wrote this comment on
        I'm getting closer - I have a page that displays a table from django_tables2, and a filter. But there's no button to make the filter take effect. If I stick the filter into the URl, such as ?foo=bar, it works, and it prepopulates the filter form. And it stays present through pagination. But I can't figure out how to make a "filter" button appear.

        So far I have the example code above, plus

        # forms.py
        #...
        from crispy_forms.helper import FormHelper

        class FooFilter(FormHelper):
        model = Foo

        and

        # models.py
        # ...
        # import django_filters

        class FooFilter(django_filters.FilterSet):
        class Meta:
        model = Foo
        fields = ['foo_field1']

        And I had to remove the "get_table" function from PagedFilteredTableView because it caused a key error on 'Page', but pagination works fine without it so I'm not sure why it was needed.

        So what do I have to add to get the "Filter" button active? Where is the filter form coming from? Is FilterSet generating it automatically as FooFilter.form?
        Reply to this comment
        1. avatar
          wrote this comment on
          Hm, I think you just want to add a submit tag?
          Reply to this comment
  3. avatar
    wrote this comment on
    (Second update) I have it working, albeit not as pretty as in your example picture. In the FormHelper class I set form_tag = False, and then I manually coded the submit button into the template as HTML:

    <form action="" method="get">
    {% crispy filter.form filter.form.helper %}
    <input type="submit" value="Filter"/>
    </form>

    What I would really like to do is put the filter control for each column into a cell in the Django_tables2 table, just above the heading cell. I guess that's advanced work.

    Also, I can't tell if there is any sanitizing happening in django_filters or elsewhere on the URL inputs.

    (my related Stack Overflow question, by the way, is here: http://stackoverflow.com/questions/25256239/how-do-i-filter-tables-with-django-generic-views)
    Reply to this comment
    1. avatar
      wrote this comment on
      Ah yes, the styling like in the picture took some tricks. In the end I just used an :after pseudo-element. The other classes necessary to style it were already there. I'm not sure about your customization idea, tweaking the little things often takes more work than the more important stuff to get something functional.

      Thanks for your analysis on SO. Yes, I omitted a lot :-)
      Reply to this comment

Start a new thread

Cancel reply
Markdown. Syntax highlighting with <code lang="php"><?php echo "Hello, world!"; ?></code> etc.