Model Definition
Create a model to represent the data you want to pagiante:
class Article(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
author = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'articles'
URL Configuration
path('articles/', views.article_list, name='article_list'),
Basic Approach: Manual Pagination in Views
from django.shortcuts import render
from .models import Article
from django.utils.safestring import mark_safe
def article_list(request):
current_page = int(request.GET.get('page', 1))
items_per_page = 10
offset = (current_page - 1) * items_per_page
limit = current_page * items_per_page
total_items = Article.objects.count()
total_pages, remainder = divmod(total_items, items_per_page)
if remainder:
total_pages += 1
dataset = Article.objects.order_by('-id')[offset:limit]
page_window = 5
if total_pages <= page_window * 2 + 1:
page_start = 1
page_end = total_pages
else:
if current_page <= page_window:
page_start = 1
page_end = page_window * 2
else:
if (current_page + page_window) > total_pages:
page_start = total_pages - page_window * 2
page_end = total_pages
else:
page_start = current_page - page_window
page_end = current_page + page_window
pagination_tags = []
pagination_tags.append(
'<li><a href="?page=1">First</a></li>'
)
if current_page > 1:
prev_tag = '<li><a href="?page={}" aria-label="Previous">‹</a></li>'.format(
current_page - 1
)
else:
prev_tag = '<li><a href="?page=1" aria-label="Previous">‹</a></li>'
pagination_tags.append(prev_tag)
for num in range(page_start, page_end + 1):
if num == current_page:
tag = '<li class="active"><a href="?page={}">{}</a></li>'.format(num, num)
else:
tag = '<li><a href="?page={}">{}</a></li>'.format(num, num)
pagination_tags.append(tag)
if current_page < total_pages:
next_tag = '<li><a href="?page={}" aria-label="Next">›</a></li>'.format(
current_page + 1
)
else:
next_tag = '<li><a href="?page={}" aria-label="Next">›</a></li>'.format(total_pages)
pagination_tags.append(next_tag)
pagination_tags.append(
'<li><a href="?page={}">Last</a></li>'.format(total_pages)
)
jump_form = '''
<li>
<form method="get" style="display: inline-block;">
<div class="input-group" style="width: 150px;">
<input name="page" type="text" class="form-control" placeholder="Go to">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Go</button>
</span>
</div>
</form>
</li>
'''
pagination_tags.append(jump_form)
page_html = mark_safe("".join(pagination_tags))
return render(request, 'articles/list.html', {
'articles': dataset,
'page_html': page_html
})
Template Implementation
<div class="article-container">
{% for article in articles %}
<div class="article-item">
<h3>{{ article.title }}</h3>
<p>{{ article.content|truncatewords:30 }}</p>
<span class="author">{{ article.author }}</span>
</div>
{% endfor %}
<nav aria-label="Page navigation">
<ul class="pagination">
{{ page_html }}
</ul>
</nav>
</div>
Advanced Approach: Reusable Pagination Component
Create a utils directory within your app and add paginate.py:
from django.utils.safestring import mark_safe
class PageNator:
def __init__(self, request, queryset, page_size=10, window=5, param='page'):
self.request = request
self.queryset = queryset
self.page_size = page_size
self.window = window
self.param = param
page_num = request.GET.get(param, '1')
if page_num.isdigit():
self.page_num = int(page_num)
else:
self.page_num = 1
self.start_index = (self.page_num - 1) * page_size
self.end_index = self.page_num * page_size
self.paginated_queryset = queryset[self.start_index:self.end_index]
total_count = queryset.count()
pages, mod = divmod(total_count, page_size)
if mod:
pages += 1
self.total_pages = pages
@property
def count(self):
return self.total_pages
def generate_range(self):
if self.total_pages <= self.window * 2 + 1:
return range(1, self.total_pages + 1)
elif self.page_num <= self.window:
return range(1, self.window * 2 + 1)
elif self.page_num + self.window >= self.total_pages:
return range(self.total_pages - self.window * 2, self.total_pages + 1)
else:
return range(self.page_num - self.window, self.page_num + self.window + 1)
def render(self):
page_range = self.generate_range()
html_parts = []
html_parts.append(
'<li><a href="?{}={}">First</a></li>'.format(self.param, 1)
)
if self.page_num > 1:
html_parts.append(
'<li><a href="?{}={}" aria-label="Previous">‹</a></li>'.format(
self.param, self.page_num - 1
)
)
else:
html_parts.append(
'<li><a href="?{}={}" aria-label="Previous">‹</a></li>'.format(self.param, 1)
)
for num in page_range:
if num == self.page_num:
html_parts.append(
'<li class="active"><a href="?{}={}">{}</a></li>'.format(
self.param, num, num
)
)
else:
html_parts.append(
'<li><a href="?{}={}">{}</a></li>'.format(self.param, num, num)
)
if self.page_num < self.total_pages:
html_parts.append(
'<li><a href="?{}={}" aria-label="Next">›</a></li>'.format(
self.param, self.page_num + 1
)
)
else:
html_parts.append(
'<li><a href="?{}={}" aria-label="Next">›</a></li>'.format(
self.param, self.total_pages
)
)
html_parts.append(
'<li><a href="?{}={}">Last</a></li>'.format(self.param, self.total_pages)
)
jump_input = '''
<li>
<form method="get">
<div class="input-group">
<input name="{}" type="text" class="form-control" placeholder="Page #">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Go</button>
</span>
</div>
</form>
</li>
'''.format(self.param)
html_parts.append(jump_input)
return mark_safe("".join(html_parts))
Usage in views:
from .utils.paginate import PageNator
def article_list(request):
queryset = Article.objects.all()
paginator = PageNator(request, queryset)
return render(request, 'articles/list.html', {
'articles': paginator.paginated_queryset,
'page_html': paginator.render()
})
Enhanced Vertion: Preserve Query Parameters
This version maintains search filters and other query parameters when navigating between pages:
from django.utils.safestring import mark_safe
from django.http.request import QueryDict
import copy
class PageNator:
def __init__(self, request, queryset, page_size=10, window=5, param='page'):
self.request = request
self.queryset = queryset
self.page_size = page_size
self.window = window
self.param = param
query_params = copy.deepcopy(request.GET)
query_params._mutable = True
self.query_params = query_params
page_num = request.GET.get(param, '1')
if page_num.isdigit():
self.page_num = int(page_num)
else:
self.page_num = 1
self.start_index = (self.page_num - 1) * page_size
self.end_index = self.page_num * page_size
self.paginated_queryset = queryset[self.start_index:self.end_index]
total_count = queryset.count()
pages, mod = divmod(total_count, page_size)
if mod:
pages += 1
self.total_pages = pages
def generate_range(self):
if self.total_pages <= self.window * 2 + 1:
return range(1, self.total_pages + 1)
elif self.page_num <= self.window:
return range(1, self.window * 2 + 1)
elif self.page_num + self.window >= self.total_pages:
return range(self.total_pages - self.window * 2, self.total_pages + 1)
else:
return range(self.page_num - self.window, self.page_num + self.window + 1)
def render(self):
page_range = self.generate_range()
html_parts = []
self.query_params.setlist(self.param, [1])
html_parts.append(
'<li><a href="?{}">First</a></li>'.format(self.query_params.urlencode())
)
if self.page_num > 1:
self.query_params.setlist(self.param, [self.page_num - 1])
html_parts.append(
'<li><a href="?{}" aria-label="Previous">‹</a></li>'.format(
self.query_params.urlencode()
)
)
else:
self.query_params.setlist(self.param, [1])
html_parts.append(
'<li><a href="?{}" aria-label="Previous">‹</a></li>'.format(
self.query_params.urlencode()
)
)
for num in page_range:
self.query_params.setlist(self.param, [num])
if num == self.page_num:
html_parts.append(
'<li class="active"><a href="?{}">{}</a></li>'.format(
self.query_params.urlencode(), num
)
)
else:
html_parts.append(
'<li><a href="?{}">{}</a></li>'.format(
self.query_params.urlencode(), num
)
)
if self.page_num < self.total_pages:
self.query_params.setlist(self.param, [self.page_num + 1])
html_parts.append(
'<li><a href="?{}" aria-label="Next">›</a></li>'.format(
self.query_params.urlencode()
)
)
else:
self.query_params.setlist(self.param, [self.total_pages])
html_parts.append(
'<li><a href="?{}" aria-label="Next">›</a></li>'.format(
self.query_params.urlencode()
)
)
self.query_params.setlist(self.param, [self.total_pages])
html_parts.append(
'<li><a href="?{}">Last</a></li>'.format(self.query_params.urlencode())
)
jump_input = '''
<li>
<form method="get">
<div class="input-group">
<input name="{}" type="text" class="form-control" placeholder="Page #">
<span class="input-group-btn">
<button class="btn btn-default" type="submit">Go</button>
</span>
</div>
</form>
</li>
'''.format(self.param)
html_parts.append(jump_input)
return mark_safe("".join(html_parts))
With this implementation, URL parameters like ?search=django&page=3 are preserved across pagination links, ensuring filters remain active when users navigate through pages.