How do I filter tables with Django generic views?

12,997

Solution 1

With trial and error and some suggestions from Nicolas Kuttler, I was able to get his example working. I butchered a little bit of his example but this does seem to be close to the least amount of code necessary in Django to have a generic class-based list view page with sorting, filtering (and thus search), and pagination, and I don't think it violates (too m)any Django coding practices. Here is all of the code required:

models.py (no changes from the question)

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

And a slight change to the tables.py in the question:

import django_tables2 as dt2
from .models import Author

class AuthorTable(dt2.Table):
    class Meta:
        model = Author
        attrs = {"class": "paleblue"}
        per_page = 30

filters.py (no changes from the question)

import django_filters as df
from .models import Author

class AuthorFilter(df.FilterSet):
    class Meta:
        model = Author

forms.py. I couldn't figure out how/where to get the form submit button out of django_filters in combination with everything else, so this code suppresses the form wrapper tags from crispy, and then we provide that HTML in the template, which is probably the kludgiest part of this.

from django import forms
from .models import Author
from crispy_forms.helper import FormHelper

class AuthorListFormHelper(FormHelper):
    model = Author
    form_tag = False

I moved the helper function out of views.py to a separate file and had to remove the pagination code to prevent an error (though pagination still works). So,

utils.py

from django_tables2 import SingleTableView
from django_tables2.config import RequestConfig

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_context_data(self, **kwargs):
        context = super(PagedFilteredTableView, self).get_context_data()
        context[self.context_filter_name] = self.filter
        return context

Instead of the views.py in the question, this views.py:

from .models import Author
from .tables import AuthorTable
from .filters import AuthorListFilter
from .forms import AuthorListFormHelper
from utils import PagedFilteredTableView

class AuthorList(PagedFilteredTableView):
    model = Author
    table_class = AuthorTable
    filter_class = AuthorListFilter
    formhelper_class = AuthorListFormHelper

And this template:

author_list.html (the name is not explicitly specified anywhere because it's implied by the model in the view, I think)

{% extends "base.html" %}
{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}

{% block content %}

{% render_table table %}

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

{% endblock content %}

And, for completeness, a line in urls.py:

    ...
    url(r'^$', views.AuthorTableList.as_view(), name='author_table'),
    ...

And you must have the packages django-tables2, django-filters, and crispy-forms installed and configured as per their instructions. The one thing that tripped me up was that I somehow, on the first attempt, missed that I needed this in settings.py:

CRISPY_TEMPLATE_PACK = 'bootstrap'

All of this is for Django 1.6.

Solution 2

Your answer help me, so i edited your code to include the submit button from crispy-forms

forms.py. I couldn't figure out how/where to get the form submit button out of django_filters in combination with everything else, so this code suppresses the form wrapper tags from crispy, and then we provide that HTML in the template, which is probably the kludgiest part of this.

forms.py (UPDATED)

from django import forms
from .models import Author
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, ButtonHolder, Submit

class AuthorListFormHelper(FormHelper):
    model = Author
    form_tag = False
    # Adding a Filter Button
    layout = Layout('name', ButtonHolder(
        Submit('submit', 'Filter', css_class='button white right')
    ))

author-list.html (UPDATED)

{% extends "base.html" %}
{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}

{% block content %}

{% render_table table %}

<hr/>
<form action="" method="get"> 
    {% crispy filter.form filter.form.helper %}
</form>

{% endblock content %}
Share:
12,997
Joel Aufrecht
Author by

Joel Aufrecht

Updated on June 26, 2022

Comments

  • Joel Aufrecht
    Joel Aufrecht about 2 years

    I am trying to create a table view with pagination, sorting, and filtering, using the most common/standard/recommended approach for Django 1.6. This seems to be Generic Class-Based Views and django-tables2. I've found at least two different examples of how to add filtering, one with django-filters and crispy-forms and the other with django_filters, but neither includes a complete working example. When I follow either approach, I get stuck filling in the missing material. Using the crispy approach from Nicolas Kuttler, I have:

    models.py

    from django.db import models
    
    class Author(models.Model):
        name = models.CharField(max_length=100)
    

    tables.py

    import django_tables2 as dt2
    from .models import Author
    
    class AuthorTable(dt2.Table):
        class Meta:
            model = Author
    

    If I understand correctly, PagedFilteredTableView is a generic class from which I then subclass AuthorView, as opposed to the other example, in which FilteredSingleTableView is, I think, supposed to be understood as something like, if Author was the table, AuthorFilteredSingleTableView.

    views.py

    from .tables import AuthorTable
    from .models import Author
    from django_tables2 import SingleTableView
    
    class PagedFilteredTableView(SingleTableView):
        """
        Generic class from http://kuttler.eu/post/using-django-tables2-filters-crispy-forms-together/
        which should probably be in a utility file
        """
        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
    
    class AuthorTableView(PagedFilteredTableView):
        model = Author
        table_class = AuthorTable
        paginate_by = 30
        filter_class = AuthorFilter
        formhelper_class = AuthorFilterFormHelper
    

    This is, aside from the template, all of the example code from the source, and manage.py is complaining that AuthorFilter isn't defined, so I guess that goes into ... maybe filters.py?

    filters.py

    import django_filters as df
    from .models import Author
    
    class AuthorFilter(df.FilterSet):
        class Meta:
            model = Author
    

    And, back in views.py, from .filters import AuthorFilter.

    And now AuthorFilterFormHelper isn't defined, and it's not clear if that's something I should explicitly define (how?), as implied by the line formhelper_class = FooFilterFormHelper, or if that should actually be done automatically somehow, as implied by self.filter.form.helper = self.formhelper_class(). And we still haven't gotten to urls.py or the template. Please help fill in the blanks, or indicate a better path to go down to add filtering to a generic class-based view.