How do I filter tables with Django generic views?
Asked Answered
P

2

6

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.

Pallet answered 12/8, 2014 at 4:36 Comment(0)
P
12

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.

Pallet answered 19/8, 2014 at 2:38 Comment(0)
G
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 %}
Guillermoguilloche answered 12/2, 2015 at 6:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.