Pagination is an essential part of platforms where one needs to list many items. Instead of displaying all the times, say 50,000 records, pagination allows the user to browse through the entire collection in steps, only seeing a part of the entire collection.
In this article, we take a look at how to get pagination working in Django via Class Based Views. Thus, if you use ListView
, for example, using pagination can give us results like this:
With that in mind, let’s get straight to it.
Our ListView
Django Class Based View
This view class is responsible for doubling as a mere listing of items in the Package
model class, plus displaying search results. Here
class PackageCustomList(ListView): model = Package template_name = 'package/custom_list.html' paginate_by = 3 def get_queryset(self, *args, **kwargs): if self.kwargs: return Package.objects.filter(category=self.kwargs['category']).order_by('-createdAt') else: query = Package.objects.all().order_by('-createdAt') return query
The key take away point from the above snippet is the part, paginate_by
. The paginate_by
attribute is available with the MultipleObjectMixin
, (docs)which is a class ListView
generic list class inherits from.
At the fundamental level, here’s what the paginate_by
field on the ListView
class is doing:
- Take the returned Queryset of the model,
Package
- Break them into separate pages, with each page holding a number of, in the case above, 3 items.
- Provide handles for switching back and forth between the different pages generated holding the items.
Therefore, if the returned Queryset of the model, Package
is made of 50 items, and the paginate_by
value is 10, then we will have 5 pages, each holding 10 items.
Enabling the paginate_by
attribute on our ListView
class gives us certain options and callbacks for free.
See a basic example of Django Pagination from the docs
Our Template
In our template, let’s play around with our page_obj
which is thrown into the template should pagination be enabled.
Remember: The
page_obj
will be available in our template if the page is ‘paginable’. As in, if you have a total of 5 items in the returned Queryset, yet you’ve stated apaginate_by
value of 100, there’s nothing to paginate, thus thepage_obj
won’t be there in the template to make use of.In fact, a boolean,
is_paginated
is trued if the returned list is worthy of paginating. We make use of that boolean next.
{% if is_paginated %} <hr> <nav aria-label="Page navigation example"> <ul class="pagination justify-content-center pagination-sm"> {% if page_obj.has_previous %} <!-- If it ain't a search result display, don't append the search query to the URL. --> {% if not search %} <li class="page-item"> <a class="page-link" href="{% url 'package_list' %}?page={{ page_obj.previous_page_number }}" tabindex="-1">Previous</a> </li> {% else %} <!-- Append the searched query to the URL, so that on a search results page, the pagination don't revert to listing all the listview items. --> <li class="page-item"> <a class="page-link" href="{% url 'package_list' %}?{{search}}&page={{ page_obj.previous_page_number }}" tabindex="-1">Previous</a> </li> {% endif %} {% else %} <li class="page-item disabled"> <a class="page-link" href="#" tabindex="-1">Previous</a> </li> {% endif %} {% for object in page_obj.paginator.page_range %} <li class="page-item"><a class="page-link" href="{% url 'package_list' %}?page={{ forloop.counter }}">{{ forloop.counter }}</a></li> {% endfor %} {% if page_obj.has_next %} {% if not search %} <li class="page-item"> <a class="page-link" href="{% url 'package_list' %}?page={{ page_obj.next_page_number }}">Next</a> </li> {% else %} <li class="page-item"> <a class="page-link" href="{% url 'package_list' %}?{{search}}&page={{ page_obj.next_page_number }}">Next</a> </li> {% endif %} {% else %} <li class="page-item disabled"> <a class="page-link" href="#">Next</a> </li> {% endif %} </ul> </nav> {% endif %}
The code snippet from above is from Bootstrap 4. It has only be altered to reflect the Django template parts, and gives us this visual look:
We make use of the:
is_paginated
bool to first check whether we have pagination available. We don’t want a pagination related UI showing without any functionality.page_obj
comes with various methods that make our job easier, such ashas_next
andhas_previous
previous_page_number
andnext_page_number
The above is fairly straightforward as to what they do. However, one feature of our pagination is the fact that we have the number of pages listed, and numbered. We use the .page_range
method available from the paginator
class to achieve this.
This part:
{% for object in page_obj.paginator.page_range %} <li class="page-item"> <a class="page-link" href="{% url 'package_list' %}?page={{ forloop.counter }}">{{ forloop.counter }}</a> </li> {% endfor %}
We loop through the page_range
, which gives a result of say, range(1,10)
accordingly.
The page_range
holds the total number of pages available. That is, if we had 50 returned queryset, and we’re paginate_by
10, our page_range
value will be range(1,6)
Using the forloop.counter
to count how many times we’re going through and displaying the value, helps us to mimic accessing the individual values of the range.
Think of it as a replacement for:
for i in range(1, 6): print(i)
in the template.
This way, we get to display the 1, 2, 3 etc number of pages, providing links to the respective pages.
Conclusion
As seen in the results video above, our pagination behaves this way:
- If there’s no previous page, disable previous page link
- If there’s no next page, disable next page link
- Display the number of pages and link to each of them
Bonus
Throw in this:
<nav aria-label="Page navigation example"> <ul class="pagination justify-content-center pagination-sm"> <li class="page-item disabled"> <a class="page-link" href="#" tabindex="-1"><small>On page <strong>{{ page_obj.number }}</strong>. Showing <strong>{{ page_obj.object_list.count }}</strong> of total <strong>{{ page_obj.paginator.count }}</strong> items.</small></a> </li> </ul> </nav>
at the top of the page, to get something like this:
I hope you enjoyed this piece. See you again in the next one.