Django: Pagination for related models using CBVs (Class Based Views)
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 List
view 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: