Pagination
A brand-new TaskFlow install might have a dozen tasks. A TaskFlow install six months into real use could easily have thousands. Rendering every single one of them on a single page load is a poor experience for whoever's scrolling through it, and a real performance problem for the server — fetching thousands of rows from the database and rendering thousands of HTML fragments on every request, for every visitor, every time. Pagination is the standard fix: split a long list into fixed-size pages, and only fetch and render the one page currently being viewed.
The Paginator Class
Django's built-in Paginator class takes any queryset (or any list-like object) and a
page size, and handles the splitting for you:
# taskflow/views.py
from django.core.paginator import Paginator
from .models import Task
def task_list(request):
tasks = Task.objects.all().order_by('due_date')
paginator = Paginator(tasks, 10)
page_obj = paginator.get_page(request.GET.get('page', 1))
return render(request, 'taskflow/task_list.html', {'page_obj': page_obj})
Paginator(tasks, 10) doesn't fetch anything from the database by itself — it just
knows the full queryset and the page size, and works out how many pages that adds up to.
Reading the Requested Page From the URL
request.GET.get('page', 1) reads a ?page= query string parameter off
the URL — visiting /tasks/?page=3 requests the third page, and visiting
/tasks/ with no query string at all falls back to page 1, thanks to that
default argument. paginator.get_page(...) then does the actual database query,
fetching only the ten rows that belong to that specific page — never the whole table.
The Page Object's Useful Properties
page_obj, the object get_page() returns, carries everything a template
needs to render both the current page's rows and the navigation controls around them:
| Property | Gives you |
|---|---|
page_obj.object_list | The actual rows for this page — the ten Task objects to loop over |
page_obj.has_next() | True if a page after this one exists |
page_obj.has_previous() | True if a page before this one exists |
page_obj.number | The current page's number |
page_obj.paginator.num_pages | The total number of pages, across the whole queryset |
Rendering Pagination Controls in a Template
page_obj can be looped over directly — Django lets it stand in for its own
object_list in a {% for %} loop — and the has-next / has-previous checks
drive the Previous and Next links, each one carrying the right page number in its own
?page= link:
<ul>
{% for task in page_obj %}
<li>{{ task.title }} — due {{ task.due_date }}</li>
{% endfor %}
</ul>
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_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 %}
</div>
The {% if %} checks around each link matter — without them, a Next link on the last
page would point at a page number that doesn't exist. Wrapping each link in its matching
has_next / has_previous check keeps the controls honest at both ends of
the list.
The Shortcut for Class-Based Views
Everything above is the manual, function-based-view approach. If TaskFlow's task list is instead
built as a ListView (covered in Class-Based
Views), pagination collapses down to a single class attribute:
# taskflow/views.py
from django.views.generic import ListView
from .models import Task
class TaskListView(ListView):
model = Task
paginate_by = 10
That one line — paginate_by = 10 — does everything the manual
Paginator setup above does: it reads ?page= from the URL, fetches only
the current page's rows, and hands the template a page_obj with the exact same
properties described above, all without a single line of pagination code in the view itself.
For the common case of paginating one model's full list, this is significantly less code than
wiring up Paginator by hand.