Pagination is essential for handling large datasets efficiently in web applications. Django provides robust pagination support through the Paginator class and built-in integration with class-based views like ListView.
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from .models import Post
def basic_pagination_example(request):
"""Basic pagination with Paginator class"""
posts = Post.objects.filter(status='published').order_by('-created_at')
# Create paginator with 10 posts per page
paginator = Paginator(posts, 10)
# Get page number from request
page_number = request.GET.get('page')
try:
page_obj = paginator.get_page(page_number)
except PageNotAnInteger:
# If page is not an integer, deliver first page
page_obj = paginator.page(1)
except EmptyPage:
# If page is out of range, deliver last page
page_obj = paginator.page(paginator.num_pages)
return render(request, 'blog/post_list.html', {
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
})
def advanced_paginator_example(request):
"""Advanced pagination with error handling"""
posts = Post.objects.filter(status='published').select_related('author')
# Create paginator with orphans handling
paginator = Paginator(posts, per_page=15, orphans=3)
page_number = request.GET.get('page', 1)
# Use get_page() for automatic error handling
page_obj = paginator.get_page(page_number)
context = {
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
'page_range': paginator.get_elided_page_range(
page_obj.number,
on_each_side=2,
on_ends=1
),
}
return render(request, 'blog/post_list.html', context)
from django.core.paginator import Paginator
def paginator_properties_demo():
"""Demonstrate Paginator properties and methods"""
posts = Post.objects.all()
paginator = Paginator(posts, 10)
# Paginator properties
print(f"Total objects: {paginator.count}")
print(f"Number of pages: {paginator.num_pages}")
print(f"Page range: {list(paginator.page_range)}")
print(f"Per page: {paginator.per_page}")
# Get specific page
page_1 = paginator.page(1)
# Page object properties
print(f"Page number: {page_1.number}")
print(f"Has next: {page_1.has_next()}")
print(f"Has previous: {page_1.has_previous()}")
print(f"Next page number: {page_1.next_page_number() if page_1.has_next() else None}")
print(f"Previous page number: {page_1.previous_page_number() if page_1.has_previous() else None}")
print(f"Start index: {page_1.start_index()}")
print(f"End index: {page_1.end_index()}")
class CustomPaginator(Paginator):
"""Custom paginator with additional features"""
def __init__(self, object_list, per_page, **kwargs):
self.show_all = kwargs.pop('show_all', False)
super().__init__(object_list, per_page, **kwargs)
def get_page(self, number):
"""Override to handle 'show all' functionality"""
if self.show_all and str(number).lower() == 'all':
# Return all objects in a single page
return self.page(1)._replace(
object_list=self.object_list,
number=1,
paginator=self._replace(num_pages=1)
)
return super().get_page(number)
def get_elided_page_range_with_context(self, number, **kwargs):
"""Enhanced page range with context information"""
page_range = self.get_elided_page_range(number, **kwargs)
return {
'page_range': page_range,
'current_page': number,
'total_pages': self.num_pages,
'has_ellipsis': '…' in page_range,
}
def custom_paginator_example(request):
"""Using custom paginator"""
posts = Post.objects.filter(status='published')
# Allow showing all results
show_all = request.GET.get('show_all') == 'true'
paginator = CustomPaginator(
posts,
per_page=20,
orphans=5,
show_all=show_all
)
page_number = request.GET.get('page', 1)
if show_all:
page_number = 'all'
page_obj = paginator.get_page(page_number)
return render(request, 'blog/post_list.html', {
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
'show_all': show_all,
})
from django.views.generic import ListView
from django.core.paginator import Paginator
class PostListView(ListView):
"""Basic paginated ListView"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
paginate_orphans = 3
ordering = ['-created_at']
def get_queryset(self):
"""Filter published posts only"""
return Post.objects.filter(
status='published'
).select_related('author', 'category')
class CategoryPostListView(ListView):
"""Paginated posts by category"""
model = Post
template_name = 'blog/category_posts.html'
context_object_name = 'posts'
paginate_by = 15
def get_queryset(self):
category_slug = self.kwargs['category_slug']
return Post.objects.filter(
category__slug=category_slug,
status='published'
).select_related('author', 'category')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add category to context
category_slug = self.kwargs['category_slug']
context['category'] = get_object_or_404(Category, slug=category_slug)
return context
class SearchPostListView(ListView):
"""Paginated search results"""
model = Post
template_name = 'blog/search_results.html'
context_object_name = 'posts'
paginate_by = 20
def get_queryset(self):
query = self.request.GET.get('q', '')
if not query:
return Post.objects.none()
return Post.objects.filter(
Q(title__icontains=query) |
Q(content__icontains=query),
status='published'
).distinct().select_related('author')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
query = self.request.GET.get('q', '')
context.update({
'query': query,
'total_results': self.get_queryset().count() if query else 0,
})
return context
class AdvancedPostListView(ListView):
"""Advanced pagination with dynamic per_page"""
model = Post
template_name = 'blog/advanced_list.html'
context_object_name = 'posts'
paginate_by = 20
paginate_orphans = 5
def get_paginate_by(self, queryset):
"""Dynamic pagination based on user preference"""
per_page = self.request.GET.get('per_page')
if per_page:
try:
per_page = int(per_page)
# Limit between 5 and 100
return max(5, min(per_page, 100))
except ValueError:
pass
# Check user preference
if self.request.user.is_authenticated:
profile = getattr(self.request.user, 'profile', None)
if profile and profile.posts_per_page:
return profile.posts_per_page
return self.paginate_by
def get_queryset(self):
queryset = Post.objects.filter(
status='published'
).select_related('author', 'category')
# Apply sorting
sort_by = self.request.GET.get('sort', 'newest')
if sort_by == 'oldest':
queryset = queryset.order_by('created_at')
elif sort_by == 'title':
queryset = queryset.order_by('title')
elif sort_by == 'author':
queryset = queryset.order_by('author__username', 'title')
elif sort_by == 'popular':
queryset = queryset.order_by('-views', '-created_at')
else: # newest
queryset = queryset.order_by('-created_at')
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add pagination info
page_obj = context.get('page_obj')
if page_obj:
context.update({
'current_page': page_obj.number,
'total_pages': page_obj.paginator.num_pages,
'total_items': page_obj.paginator.count,
'start_index': page_obj.start_index(),
'end_index': page_obj.end_index(),
'per_page_options': [10, 20, 50, 100],
'current_per_page': self.get_paginate_by(None),
'page_range': self.get_page_range(page_obj),
})
# Add current sort
context['current_sort'] = self.request.GET.get('sort', 'newest')
return context
def get_page_range(self, page_obj):
"""Get smart page range for pagination"""
paginator = page_obj.paginator
current_page = page_obj.number
# Use elided page range for large page counts
if paginator.num_pages > 10:
return paginator.get_elided_page_range(
current_page,
on_each_side=2,
on_ends=1
)
return paginator.page_range
class AjaxPostListView(ListView):
"""AJAX-enabled pagination"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 12
def get_queryset(self):
return Post.objects.filter(
status='published'
).select_related('author')
def render_to_response(self, context, **response_kwargs):
"""Handle AJAX pagination requests"""
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# Return JSON for AJAX requests
page_obj = context.get('page_obj')
posts_data = []
for post in context['posts']:
posts_data.append({
'id': post.id,
'title': post.title,
'excerpt': post.excerpt,
'author': post.author.username,
'created_at': post.created_at.strftime('%B %d, %Y'),
'url': post.get_absolute_url(),
'featured_image': post.featured_image.url if post.featured_image else None,
})
pagination_data = {
'posts': posts_data,
'pagination': {
'current_page': page_obj.number if page_obj else 1,
'total_pages': page_obj.paginator.num_pages if page_obj else 1,
'has_next': page_obj.has_next() if page_obj else False,
'has_previous': page_obj.has_previous() if page_obj else False,
'next_page_number': page_obj.next_page_number() if page_obj and page_obj.has_next() else None,
'previous_page_number': page_obj.previous_page_number() if page_obj and page_obj.has_previous() else None,
'total_items': page_obj.paginator.count if page_obj else 0,
'start_index': page_obj.start_index() if page_obj else 0,
'end_index': page_obj.end_index() if page_obj else 0,
}
}
return JsonResponse(pagination_data)
return super().render_to_response(context, **response_kwargs)
class InfiniteScrollListView(ListView):
"""Infinite scroll pagination"""
model = Post
template_name = 'blog/infinite_scroll.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(
status='published'
).select_related('author')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add infinite scroll data
page_obj = context.get('page_obj')
if page_obj:
context.update({
'has_more': page_obj.has_next(),
'next_page_url': f"?page={page_obj.next_page_number()}" if page_obj.has_next() else None,
'current_page': page_obj.number,
})
return context
def render_to_response(self, context, **response_kwargs):
"""Handle infinite scroll AJAX requests"""
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# Return partial template for AJAX
self.template_name = 'blog/partials/post_list_items.html'
return super().render_to_response(context, **response_kwargs)
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from django.db.models import Q, Count
def post_list_view(request):
"""Basic function-based view with pagination"""
posts = Post.objects.filter(status='published').order_by('-created_at')
paginator = Paginator(posts, 15) # 15 posts per page
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
}
return render(request, 'blog/post_list.html', context)
def filtered_post_list_view(request):
"""Function-based view with filtering and pagination"""
posts = Post.objects.filter(status='published')
# Apply filters
category_slug = request.GET.get('category')
if category_slug:
posts = posts.filter(category__slug=category_slug)
author_id = request.GET.get('author')
if author_id:
posts = posts.filter(author_id=author_id)
tag_slug = request.GET.get('tag')
if tag_slug:
posts = posts.filter(tags__slug=tag_slug)
search_query = request.GET.get('q')
if search_query:
posts = posts.filter(
Q(title__icontains=search_query) |
Q(content__icontains=search_query)
)
# Apply ordering
sort_by = request.GET.get('sort', 'newest')
if sort_by == 'oldest':
posts = posts.order_by('created_at')
elif sort_by == 'title':
posts = posts.order_by('title')
elif sort_by == 'popular':
posts = posts.order_by('-views')
else:
posts = posts.order_by('-created_at')
# Optimize query
posts = posts.select_related('author', 'category').prefetch_related('tags')
# Pagination
per_page = request.GET.get('per_page', 20)
try:
per_page = int(per_page)
per_page = max(5, min(per_page, 100)) # Between 5 and 100
except ValueError:
per_page = 20
paginator = Paginator(posts, per_page)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
'current_filters': {
'category': category_slug,
'author': author_id,
'tag': tag_slug,
'search': search_query,
'sort': sort_by,
'per_page': per_page,
},
'filter_options': get_filter_options(),
}
return render(request, 'blog/filtered_list.html', context)
def get_filter_options():
"""Get available filter options"""
return {
'categories': Category.objects.annotate(
post_count=Count('posts', filter=Q(posts__status='published'))
).filter(post_count__gt=0),
'authors': User.objects.filter(
posts__status='published'
).annotate(post_count=Count('posts')).distinct(),
'popular_tags': Tag.objects.annotate(
post_count=Count('posts', filter=Q(posts__status='published'))
).filter(post_count__gt=0).order_by('-post_count')[:20],
}
def ajax_post_list_view(request):
"""AJAX-enabled function-based pagination"""
posts = Post.objects.filter(status='published').select_related('author')
# Apply search if provided
search_query = request.GET.get('q', '')
if search_query:
posts = posts.filter(
Q(title__icontains=search_query) |
Q(content__icontains=search_query)
)
posts = posts.order_by('-created_at')
# Pagination
paginator = Paginator(posts, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
# Handle AJAX requests
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
posts_data = []
for post in page_obj:
posts_data.append({
'id': post.id,
'title': post.title,
'excerpt': post.excerpt,
'author': post.author.get_full_name() or post.author.username,
'created_at': post.created_at.strftime('%B %d, %Y'),
'url': post.get_absolute_url(),
'category': post.category.name if post.category else None,
})
return JsonResponse({
'posts': posts_data,
'pagination': {
'current_page': page_obj.number,
'total_pages': paginator.num_pages,
'has_next': page_obj.has_next(),
'has_previous': page_obj.has_previous(),
'total_items': paginator.count,
'start_index': page_obj.start_index(),
'end_index': page_obj.end_index(),
},
'search_query': search_query,
})
# Regular template response
context = {
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
'search_query': search_query,
}
return render(request, 'blog/ajax_list.html', context)
def category_posts_view(request, category_slug):
"""Category-specific posts with pagination"""
category = get_object_or_404(Category, slug=category_slug)
posts = Post.objects.filter(
category=category,
status='published'
).select_related('author').order_by('-created_at')
paginator = Paginator(posts, 12)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'category': category,
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
}
return render(request, 'blog/category_posts.html', context)
def user_posts_view(request, username):
"""User-specific posts with pagination"""
author = get_object_or_404(User, username=username)
posts = Post.objects.filter(
author=author,
status='published'
).select_related('category').order_by('-created_at')
paginator = Paginator(posts, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
# Get author statistics
author_stats = {
'total_posts': posts.count(),
'total_views': posts.aggregate(total_views=Sum('views'))['total_views'] or 0,
'categories': posts.values('category__name').annotate(
count=Count('id')
).order_by('-count')[:5],
}
context = {
'author': author,
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
'author_stats': author_stats,
}
return render(request, 'blog/user_posts.html', context)
def paginated_api_view(request):
"""API endpoint with pagination"""
posts = Post.objects.filter(status='published').select_related('author')
# Get pagination parameters
page = request.GET.get('page', 1)
per_page = request.GET.get('per_page', 20)
try:
per_page = int(per_page)
per_page = max(1, min(per_page, 100)) # Limit between 1 and 100
except ValueError:
per_page = 20
paginator = Paginator(posts, per_page)
page_obj = paginator.get_page(page)
# Serialize posts
posts_data = []
for post in page_obj:
posts_data.append({
'id': post.id,
'title': post.title,
'slug': post.slug,
'excerpt': post.excerpt,
'author': {
'id': post.author.id,
'username': post.author.username,
'full_name': post.author.get_full_name(),
},
'created_at': post.created_at.isoformat(),
'updated_at': post.updated_at.isoformat(),
'url': request.build_absolute_uri(post.get_absolute_url()),
})
# Build pagination links
base_url = request.build_absolute_uri(request.path)
def build_page_url(page_num):
params = request.GET.copy()
params['page'] = page_num
return f"{base_url}?{params.urlencode()}"
pagination_links = {
'first': build_page_url(1),
'last': build_page_url(paginator.num_pages),
'next': build_page_url(page_obj.next_page_number()) if page_obj.has_next() else None,
'previous': build_page_url(page_obj.previous_page_number()) if page_obj.has_previous() else None,
}
response_data = {
'posts': posts_data,
'pagination': {
'current_page': page_obj.number,
'per_page': per_page,
'total_pages': paginator.num_pages,
'total_items': paginator.count,
'has_next': page_obj.has_next(),
'has_previous': page_obj.has_previous(),
'links': pagination_links,
}
}
return JsonResponse(response_data)
def cursor_paginated_view(request):
"""Cursor-based pagination for real-time feeds"""
cursor = request.GET.get('cursor')
limit = min(int(request.GET.get('limit', 20)), 100)
posts = Post.objects.filter(status='published').order_by('-created_at')
if cursor:
try:
# Decode cursor (base64 encoded timestamp)
import base64
from datetime import datetime
cursor_time = datetime.fromisoformat(
base64.b64decode(cursor).decode('utf-8')
)
posts = posts.filter(created_at__lt=cursor_time)
except (ValueError, TypeError):
pass # Invalid cursor, ignore
# Get one extra to check if there are more results
posts_list = list(posts[:limit + 1])
has_more = len(posts_list) > limit
if has_more:
posts_list = posts_list[:limit]
# Generate next cursor
next_cursor = None
if has_more and posts_list:
last_post = posts_list[-1]
next_cursor = base64.b64encode(
last_post.created_at.isoformat().encode('utf-8')
).decode('utf-8')
# Serialize posts
posts_data = []
for post in posts_list:
posts_data.append({
'id': post.id,
'title': post.title,
'created_at': post.created_at.isoformat(),
'author': post.author.username,
})
return JsonResponse({
'posts': posts_data,
'pagination': {
'has_more': has_more,
'next_cursor': next_cursor,
'limit': limit,
}
})
def bulk_paginated_view(request):
"""Pagination with bulk operations"""
posts = Post.objects.filter(status='published').select_related('author')
# Handle bulk actions
if request.method == 'POST':
action = request.POST.get('action')
selected_ids = request.POST.getlist('selected_posts')
if action == 'bulk_delete' and selected_ids:
Post.objects.filter(
id__in=selected_ids,
author=request.user # Only allow deleting own posts
).delete()
elif action == 'bulk_feature' and selected_ids:
Post.objects.filter(
id__in=selected_ids,
author=request.user
).update(featured=True)
# Redirect to avoid resubmission
return redirect(request.path)
# Pagination
paginator = Paginator(posts, 25)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'posts': page_obj,
'paginator': paginator,
'page_obj': page_obj,
'bulk_actions': [
('bulk_delete', 'Delete Selected'),
('bulk_feature', 'Feature Selected'),
],
}
return render(request, 'blog/bulk_list.html', context)
<!-- blog/post_list.html -->
<div class="post-list">
{% for post in posts %}
<article class="post-item">
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p class="post-meta">
By {{ post.author.get_full_name|default:post.author.username }}
on {{ post.created_at|date:"F d, Y" }}
</p>
<p>{{ post.excerpt }}</p>
</article>
{% empty %}
<p>No posts found.</p>
{% endfor %}
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<nav class="pagination" aria-label="Page navigation">
<ul class="pagination-list">
{% if page_obj.has_previous %}
<li>
<a href="?page=1" class="pagination-link" aria-label="First page">
« First
</a>
</li>
<li>
<a href="?page={{ page_obj.previous_page_number }}"
class="pagination-link" aria-label="Previous page">
‹ Previous
</a>
</li>
{% endif %}
{% for page_num in page_obj.paginator.page_range %}
{% if page_num == page_obj.number %}
<li>
<span class="pagination-link current" aria-current="page">
{{ page_num }}
</span>
</li>
{% else %}
<li>
<a href="?page={{ page_num }}" class="pagination-link">
{{ page_num }}
</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li>
<a href="?page={{ page_obj.next_page_number }}"
class="pagination-link" aria-label="Next page">
Next ›
</a>
</li>
<li>
<a href="?page={{ page_obj.paginator.num_pages }}"
class="pagination-link" aria-label="Last page">
Last »
</a>
</li>
{% endif %}
</ul>
</nav>
<div class="pagination-info">
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }}
of {{ page_obj.paginator.count }} posts
</div>
{% endif %}
<!-- blog/advanced_pagination.html -->
{% load custom_tags %}
<div class="pagination-controls">
<!-- Per page selector -->
<div class="per-page-selector">
<label for="per-page">Posts per page:</label>
<select id="per-page" onchange="changePerPage(this.value)">
{% for option in per_page_options %}
<option value="{{ option }}"
{% if option == current_per_page %}selected{% endif %}>
{{ option }}
</option>
{% endfor %}
</select>
</div>
<!-- Page info -->
<div class="page-info">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
({{ page_obj.paginator.count }} total posts)
</div>
</div>
<!-- Smart pagination with elided ranges -->
{% if page_obj.has_other_pages %}
<nav class="smart-pagination">
<ul class="pagination-list">
{% if page_obj.has_previous %}
<li><a href="?{% url_params page=1 %}">«</a></li>
<li><a href="?{% url_params page=page_obj.previous_page_number %}">‹</a></li>
{% endif %}
{% for page_num in page_range %}
{% if page_num == page_obj.number %}
<li><span class="current">{{ page_num }}</span></li>
{% elif page_num == '…' %}
<li><span class="ellipsis">…</span></li>
{% else %}
<li><a href="?{% url_params page=page_num %}">{{ page_num }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li><a href="?{% url_params page=page_obj.next_page_number %}">›</a></li>
<li><a href="?{% url_params page=page_obj.paginator.num_pages %}">»</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
<!-- AJAX pagination -->
<script>
function changePerPage(perPage) {
const url = new URL(window.location);
url.searchParams.set('per_page', perPage);
url.searchParams.set('page', '1'); // Reset to first page
window.location.href = url.toString();
}
// AJAX page loading
document.addEventListener('click', function(e) {
if (e.target.matches('.pagination-list a')) {
e.preventDefault();
loadPage(e.target.href);
}
});
function loadPage(url) {
fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
updatePostList(data.posts);
updatePagination(data.pagination);
// Update URL without page reload
history.pushState(null, '', url);
})
.catch(error => {
console.error('Error loading page:', error);
});
}
function updatePostList(posts) {
const container = document.querySelector('.post-list');
container.innerHTML = posts.map(post => `
<article class="post-item">
<h2><a href="${post.url}">${post.title}</a></h2>
<p class="post-meta">By ${post.author} on ${post.created_at}</p>
<p>${post.excerpt}</p>
</article>
`).join('');
}
function updatePagination(pagination) {
// Update pagination controls based on pagination data
// Implementation depends on your specific needs
}
</script>
Django's pagination system provides flexible tools for handling large datasets efficiently. The Paginator class offers fine-grained control, while ListView provides convenient built-in pagination. Understanding these tools enables you to create responsive, user-friendly interfaces for browsing large collections of data.
Asynchronous Class-Based Views
Django's support for asynchronous views allows you to handle I/O-bound operations more efficiently by using Python's async/await syntax. Asynchronous class-based views are particularly useful when dealing with external APIs, database queries, or any operations that involve waiting for responses.
Forms and User Input
Django's form handling system provides a comprehensive framework for processing user input, validating data, and rendering HTML forms. This chapter covers everything from basic form creation to advanced techniques for handling complex user interactions.