Django: Pagination for related models using CBVs (Class Based Views)

Adrien Van Thong
3 min readJan 13, 2023

--

Django provides a powerful built-in construct called Paginator which helps streamline handling pagination for your models and helps eliminate boilerplate code. This article will illustrate how to utilize this class in the Django built-in ListView and DetailView CBVs in two simple examples.

Example models

First, let’s define the model class and the corresponding view without any pagination.

For a simple example let’s represent a simple use case of packing boxes with items in each box. The relationship will be represented as a one-to-many foreign key relationship on the Item model pointing to the Box model.

class Box(models.model):
name = models.CharField(max_length=128)
height = models.IntegerField()
width = models.IntegerField()
length = models.IntegerField()
description = models.TextField()

class Item(models.model):
box = models.ForeignKey(Box)
name = models.CharField(max_length=128)
description = models.TextField()

Next, are the corresponding un-paginated views for the Box model:

class BoxListView(ListView):
model = Box

class BoxDetailView(DetailView):
model = Box
slug_field = 'id'
slug_url_kwarg = 'id'
context_object_name = 'box'

Now, let’s paginate both views, starting with the ListView.

Pagination of a model in a ListView

First step is to paginate the Listview for the Box model so that the list of all boxes is broken up into pages with 15 boxes per page.

Luckily, Django makes this extremely easy by providing the paginate_by property in the ListView generic class. Below is such an example of how to update the existing ListView for paginating the boxes in groups of 15:

class BoxListView(ListView):
model = Box
paginate_by = 15

Utilizing the paginate_by property in the CBV unlocks access to new context variables in the template to allow rendering of pages, like so:

<ul>
{% for box in page_obj %}
<li>{{ box.name }}</li>
{% endfor %}
</ul>
<br />
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.prev_page_number }}">Previous</a>
{% endif %}
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}

The page_obj in the for loop is a generator for instances of the Box model and can be referenced as such. Only the instances for the current page will be returned by the generator.

Note that the CBV has automatically inherited a new HTTP GET parameter, page, representing the current page number being displayed.

Pagination for a related model in a DetailView

Next is the more complicated problem: when showing the details page for a single box, let’s paginate all of the contents of that box (represented by the Item model) while in the context of the BoxDetails view. After all, a single Box may contain hundreds of items.

The answer is to combine overwriting the get_context_object method in the CBV with the Paginator class built-in to Django:

class BoxDetailView(DetailView):
model = Box
slug_field = 'box_id'
slug_url_kwarg = 'box_id'
context_object_name = 'box'

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
box = kwargs.get('object')
page_num = self.request.GET.get('page', 1)
results = box.item_set.all()
paginator = Paginator(results, per_page=15)
context['box_contents'] = paginator.get_page(page_num)
return context

The updated method adds a new variable to the context dictionary being sent to the template, called box_contents. This new variable is an instance of Django’s Page object which, like in the previous example, contains all of the contents of the current page, as well as some metadata about the current page number, total number of pages, next and previous page numbers, and so on.

In the template, the new variable can also be utilized to print each of the items in the box, for the current page only:

<h1>Viewing details for box {{ box.name }}</h1>
Box dimensions: {{ box.width }}"W x {{ box.length }}"L x {{ box.height }}"H
<br />
Description: {{ box.description }}
<br />
<h2>Box contents:</h2>
<ul>
{% for item in box_contents %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>

Below that, in the same template file, the following can be used to print all of the page numbers, and to highlight the current page in bold font:

Pages:
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.prev_page_number }}">Previous</a>
{% endif %}
{% for page_num in box_contents.paginator.page_range %}
{% if page_num == box_contents.number %}
<strong>{{ page_num }}</strong>
{% else %}
<a href="?page={{ page_num }}">{{ page_num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}

Happy paginating! For further examples and deeper dives into the subject, check out the links below.

Recommended Readings

I found the following pages to be extremely helpful:

--

--