Django: django-tables2 pagination and filtering

Dirty Penguin picture Dirty Penguin · Jan 2, 2014 · Viewed 12.7k times · Source

I have a working table generated by django-tables2:

my_filter = TestFilter(request.POST) 
table = TestTable(TestObj.objects.all(), order_by="-my_date")
RequestConfig(request, paginate={"per_page": 10}).configure(table)
return render(request, 'test_app/index.html', {'table': table, 'my_filter': my_filter})

The above code returns a table with hundreds of objects that are neatly paginated with 10 items per page. When I click "Next" at the bottom of the table, pagination works well and I can navigate through different pages. However, I noticed the following behavior:

  • Click on my_filter which displays a subset of the original unfiltered table
  • Click "Next" on the bottom of the filtered table will result in displaying the 2nd page of the unfiltered table
  • Click on my_filter again displays the 2nd page of the filtered table

I would like the filter to persist while navigating different pages. I found a similar question here. That solution indicates that the html code needs to be altered. However, in my case django-tables2 is generating the html.

How can I correctly implement pagination with filtering using django-tables2?

-Update-

I've tried using GET instead of POST:

if request.method == 'GET':
    my_filter = TestFilter(request.GET)
    my_choice = my_filter.data['my_choice']
    table = TestTable(TestObj.objects.filter(choice=my_choice), order_by="-my_date")
    RequestConfig(request, paginate={"per_page": 10}).configure(table)
    return render(request, 'test_app/index.html', {'table': table, 'my_filter': my_filter})

My template:

<form action="" method="get"> {% csrf_token %}
    {{ my_filter }} <input type="submit" value="Apply Filter"/>
</form>

This results in a KeyError due to my_choice not existing in GET. As a result the page does not even load.

Answer

Serafeim picture Serafeim · Jan 3, 2014

Which version of django_tables2 are you using? I checked the source and saw that django_tables2 is using a template tag named querystring for creating the pagination links in the table.html template. the querystring tag updates the current url with the paging parameters. so django_tables2 supports pagination + filtering out of the box (that's what I remembered).

Please try updating to the latest version of django_tables2 and make sure that you are using the default table.html template for rendering your tables.

Also are you submitting your filter form with GET or POST? Please make sure that submit it with GET!

Finally, please take a look at my answer to this question Django Tables - Column Filtering

update: I took a closer look in the code you posted: first of all, you are passing post data to the filter: you cannot use POST for that, POST has to be used only for actions that modify your data. also I saw that you do not filter anything but instead you pass .all() to the table! where is the actual filtering done? you should pass the filtered data to the table as I describe in the answer above!

update 2: The problem with your view is that when you first visit the page the GET dictionary does not contain the my_choice attribute so it will throw an exception when trying to access the my_choice attribute through the [] operator, so you should check to see if it actually exists using for instance .get(), something like this:

my_filter = TestFilter(request.GET)
my_choice = my_filter.data.get('my_choice') # This won't throw an exception
if my_choice: # If my_choice existed on the GET dictionary this will return non-null value
    table = TestTable(TestObj.objects.filter(choice=my_choice), order_by="-my_date")
else:
    table = TestTable(TestObj.objects.all(), order_by="-my_date")
RequestConfig(request, paginate={"per_page": 10}).configure(table)
return render(request, 'test_app/index.html', {'table': table, 'my_filter': my_filter})

The above should work, however by doing do queryset filtering yourself - you are violating nearly every django design philosophy !

That's why I told you to read my other answer to the similar question (Django Tables - Column Filtering) in which I recommend using django-filter which is a package explicitly used for filtering querysets. Please check the documentation or my answer to see how it can be used (I'd be happy to help if you have questions).

Also, there is a number of other minor problems with your code:

  • You don't need to check if the request.method is GET - it will always be GET since you won't do any POSTs

  • You shouldn't include {{ csrf_token }} to your template - it is only needed for POST.

  • The TestFilter class is actually a Form that's why I recommend naming it TestFilterForm or something similar -- if you'd used django-filter then you'd create a FilterSet class which'd be named TestFilter. Correct naming of classes is very important, when I first saw your code I thought that the TestFilter class was a FilterSet and not a Form !