Django Filters with Pagination
How to add pagination at the bottom of the page

Search for a command to run...
How to add pagination at the bottom of the page

No comments yet. Be the first to comment.
I'm working on a project in my new job and as part of the project I'm trying to register some components globally. As a company we're going to build out a component library and we want to be able to use some components without having to manually impo...

In the last blog post I built the functionality to handle form submission for the file upload itself, but we still didn't have a way to actually handle the files. In this post I'll set up the very basic markup as well as the accompanying script funct...

In the last blog post I touched on setting up a Google Drive folder to serve as the location of our uploads as well as creating the necessary endpoints for uploading and renaming the files. In this post I'll set up functions to send any necessary fil...

In the last blog post I set up a service account to authenticate for Google Drive's API, encrypted the resulting .json file, and set up functionality to decrypt said file to use for the purpose of actual authentication. In this post I will show you h...

Note: all credentials have already been destroyed so there is no need to let me know to not share credential information online. I just finished working on a project for a client/friend in which she wanted to have a page where she could have forms av...

As I mentioned in my last post I tend to always have this desire to go back to Django, and I'm sort of acting on that now. I wrote a full API for the weight tracking app that I mentioned in that post in FastAPI only to realize that I didn't want to deal with the baggage of a front end framework for something that should work very well in an MVC setup. Thankfully Django and FastAPI are both Python so a lot of the functionality ports over well.
That said, I did run into a weird little hiccup earlier today and I thought I'd share what I ended up doing. I have a view that lists out all foods that are either system foods or user-created foods, but as I have a fairly large database of foods to begin with, since I had kept this in a spreadsheet before and I scraped all of the restaurant foods from http://www.exercise4weightloss.com/weight-watchers-points.html, it amounted to a list of just shy of 50,000 foods so I clearly needed to add pagination.
This is easy to do in Django if you're using a ListView in that you only need to add one line to your class-based view
# food/views.py
from django.db.models import QuerySet, Q
from django.views.generic import ListView
from .models import Food
class FoodListView(ListView):
model = Food
paginate_by = 20
def get_queryset(self) -> QuerySet[Food]:
self.queryset = Food.objects.filter(
Q(user_created=False) | Q(user=self.request.user)
)
return super().get_queryset()
The get_queryset method is to set it to return all system foods and the foods created by the logged in user.
Then you simply need to update your template to include pagination as described in https://docs.djangoproject.com/en/4.0/topics/pagination/#paginating-a-listview
{# food_list.html #}
{% extends 'base.html' %}
{% block content %}
<ul>
{% for food in food_list %}
<li>{{ food }}</li>
{% endfor %}
</ul>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">« first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last »</a>
{% endif %}
</span>
</div>
{% endblock content %}
If you want to include filtering, though, and you don't want to manually handle it, this can create a problem.
The first thing you would need to do is install django-filter. Then after that you can simply create a filter and replace your ListView with a FilterView
# food/views.py
import django_filters
from django.db.models import QuerySet, Q
from django_filters.views import FilterView
from .models import Food
class FoodFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr="icontains")
class Meta:
model = Food
fields = ["name"]
class FoodListView(FilterView):
model = Food
paginate_by = 20
filterset_class = FoodFilter
template_name_suffix = "_list"
def get_queryset(self) -> QuerySet[Food]:
self.queryset = Food.objects.filter(
Q(user_created=False) | Q(user=self.request.user)
)
return super().get_queryset()
This, in theory, should return the same functionality as before, but now we can call http://localhost:8000/foods/?name=chocolate or anything like that and it will properly filter the queryset.
The issue that we run into, though, has to do with going to the next page. Currently, the next and previous page links on the template don't have any way of knowing what the current query is. So, while the first link will go to http://localhost:8000/foods/?name=chocolate but if I click on the "next page" link it will go to http://localhost:8000/foods/?page=2 and it will forget about the filter that I applied.
The simplest way I found to do this, as of this moment, is two-fold.
Within the view itself we need to add a get_context_data method
class FoodListView(FilterView):
model = Food
paginate_by = 20
filterset_class = FoodFilter
template_name_suffix = "_list"
def get_queryset(self) -> QuerySet[Food]:
self.queryset = Food.objects.filter(
Q(user_created=False) | Q(user=self.request.user)
)
return super().get_queryset()
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context["query"] = dict()
for k, v in context["filter"].data.items():
if k != "page":
context["query"][k] = v
return context
What this will do is it will check to see what the items are in the QueryDict that Django filter uses, and create a query object on the context that will include all existing query parameters but not including any that have page as the key because we want to let the built-in pagination handle that.
Then we just update the template to include all of these query objects on the next and prev anchor tags:
{# food_list.html #}
{% extends 'base.html' %}
{% block content %}
<ul>
{% for food in food_list %}
<li>{{ food }}</li>
{% endfor %}
</ul>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1{% for k, v in query.items %}&{{ k }}={{ v }}{% endfor %}">« first</a>
<a href="?page={{ page_obj.previous_page_number }}{% for k, v in query.items %}&{{ k }}={{ v }}{% endfor %}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}{% for k, v in query.items %}&{{ k }}={{ v }}{% endfor %}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}{% for k, v in query.items %}&{{ k }}={{ v }}{% endfor %}">last »</a>
{% endif %}
</span>
</div>
{% endblock content %}
What this will now do is append an &k=v for each key/value pair that previously existed in the Django filter query. I'm sure I'm going to abstract this out into a snippet that will be more easily reusable down the road, but for now this was the easiest way to get it working quickly.