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:

from django.views.generic import TemplateView
from django_tables2 import RequestConfig
# Your write the four classes imported below
# (and pick a better location)
from foo.models import Foo
from foo.models import FooFilter
from foo.models import FooTable
from foo.models import FooFilterFormHelper

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 your FooFilter (django_filters.FilterSet) 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 (crispy_forms.helper.FormHelper) is added to style and configure the form.

At last the table (django_tables2.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

More

The code above should really be all you need, assuming that you read the app-specific documentation.

A simple table might look like this:

class FooTable(django_tables2.Table):
    def render_name(self, value, record):
        url = record.get_absolute_url()
        return mark_safe('<a href="%s">%s</a>' % (url, record))

    class Meta:
        model = Foo
        fields = ('name', 'attribute1', )

This is an example for a simple filter:

class FooFilter(django_filters.FilterSet):
    attribute1 = django_filters.NumberFilter(lookup_type='exact')

    class Meta:
        model = Foo
        fields = ('name', 'attribute1', )

And here's a simple crispy form helper for the filter controls:

class FooFilterFormHelper(crispy_forms.helper.FormHelper):
    form_method = 'GET'
    layout = Layout(
        'name',
        'attribute1',
        Submit('submit', 'Apply Filter'),
    )

Rendering and styling of course depends on your projects and preferences. The screenshot uses bootstrap, please refer to the crispy-forms documentation.

18 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 :(
  2. avatar
    wrote this comment on
    The filter is displayed with the {% crispy %} template tag, that's all there is to it.
  3. 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?
  4. 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!
  5. 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?
  6. 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:
    {% crispy filter.form filter.form.helper %}
    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)
  7. avatar
    wrote this comment on
    Hm, I think you just want to add a submit tag?
  8. 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 :-)
  9. avatar
    wrote this comment on
    Very nice code. Just one question. Is it possible to somehow access the request.user in the class view: FooTableView(PagedFilteredTableView)?
  10. avatar
    wrote this comment on
    Yes, the view's inheritance tree goes back to Django's View class, so the request is passed to the get() and post() methods.
  11. avatar
    wrote this comment on
    Hi thx for the example for the line
    {% crispy filter.form filter.form.helper %} 

    I get an error helper object provided to {% crispy %} tag must be a crispy.helper.FormHelper object. any suggestions?
  12. avatar
    wrote this comment on
    Hm, hard to tell without seeing all your code. You should probably inspect whatever you're passing as filter.form and filter.form.helper to the view.
  13. avatar
    wrote this comment on
    Hi, this looks great, thanks. Too bad you didn't share the project sources though, I've trouble figuring out how to create the models.
  14. avatar
    wrote this comment on
    could anyone please share the project? I'm a new in django...will it work on python 3 env?
  15. avatar
    wrote this comment on
    FooFilterFormHelper how to write?
  16. avatar
    wrote this comment on
    It's very cruel to share only a half of your work! But thanks anyway. It helps :)
  17. avatar
    wrote this comment on
    Hi! Thank you for the write-up, very informative. I got it working, but now I have another filtering problem. I want to be able to dynamically show and hide table columns. There will be another filter form, for choosing which columns to display. Do you have any tips on how to do that? I will appreciate any advice!
  18. avatar
    wrote this comment on
    I am getting __init__() takes 1 positional argument but 2 were given error

Reply

Cancel reply
Markdown. Syntax highlighting with <code lang="php"><?php echo "Hello, world!"; ?></code> etc.
DjangoPythonBitcoinTuxDebianHTML5 badgeSaltStackUpset confused bugMoneyHackerUpset confused bugX.OrggitFirefoxWindowMakerBashIs it worth the time?i3 window managerWagtailContainerIrssiNginxSilenceUse a maskWorldInternet securityPianoFontGnuPGThunderbirdJenkinshome-assistant-logo