Django's built-in generic views provide powerful, reusable patterns for common web application tasks. These views handle the most frequent operations like displaying lists of objects, showing object details, and managing object lifecycle operations.
from django.views.generic import ListView
from django.db.models import Q, Count
from django.core.paginator import Paginator
class PostListView(ListView):
"""Basic post list view"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
ordering = ['-created_at']
def get_queryset(self):
"""Filter published posts only"""
return Post.objects.filter(status='published').select_related('author', 'category')
class CategoryPostListView(ListView):
"""Posts filtered by category"""
model = Post
template_name = 'blog/category_posts.html'
context_object_name = 'posts'
paginate_by = 12
def get_queryset(self):
"""Filter posts by category"""
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):
"""Add category to context"""
context = super().get_context_data(**kwargs)
category_slug = self.kwargs['category_slug']
try:
context['category'] = Category.objects.get(slug=category_slug)
except Category.DoesNotExist:
raise Http404("Category not found")
return context
class SearchPostListView(ListView):
"""Search posts with query"""
model = Post
template_name = 'blog/search_results.html'
context_object_name = 'posts'
paginate_by = 15
def get_queryset(self):
"""Search posts based on query"""
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) |
Q(tags__name__icontains=query),
status='published'
).distinct().select_related('author', 'category')
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 post list with filtering and sorting"""
model = Post
template_name = 'blog/advanced_list.html'
context_object_name = 'posts'
paginate_by = 20
paginate_orphans = 3
def get_queryset(self):
"""Advanced filtering and sorting"""
queryset = Post.objects.filter(status='published').select_related(
'author', 'category'
).prefetch_related('tags')
# Apply filters from GET parameters
queryset = self.apply_filters(queryset)
# Apply sorting
queryset = self.apply_sorting(queryset)
return queryset
def apply_filters(self, queryset):
"""Apply various filters"""
# Category filter
category = self.request.GET.get('category')
if category:
queryset = queryset.filter(category__slug=category)
# Author filter
author = self.request.GET.get('author')
if author:
queryset = queryset.filter(author__username=author)
# Tag filter
tag = self.request.GET.get('tag')
if tag:
queryset = queryset.filter(tags__slug=tag)
# Date range filter
date_from = self.request.GET.get('date_from')
date_to = self.request.GET.get('date_to')
if date_from:
queryset = queryset.filter(created_at__gte=date_from)
if date_to:
queryset = queryset.filter(created_at__lte=date_to)
# Featured filter
featured = self.request.GET.get('featured')
if featured == 'true':
queryset = queryset.filter(featured=True)
return queryset
def apply_sorting(self, queryset):
"""Apply sorting based on GET parameter"""
sort_by = self.request.GET.get('sort', 'newest')
sort_options = {
'newest': ['-created_at'],
'oldest': ['created_at'],
'title': ['title'],
'author': ['author__username', 'title'],
'popular': ['-views', '-created_at'],
'comments': ['-comment_count', '-created_at'],
}
# Add comment count annotation for comment sorting
if sort_by == 'comments':
queryset = queryset.annotate(
comment_count=Count('comments', filter=Q(comments__approved=True))
)
ordering = sort_options.get(sort_by, ['-created_at'])
return queryset.order_by(*ordering)
def get_context_data(self, **kwargs):
"""Add filter and sort context"""
context = super().get_context_data(**kwargs)
# Add current filters
context.update({
'current_category': self.request.GET.get('category', ''),
'current_author': self.request.GET.get('author', ''),
'current_tag': self.request.GET.get('tag', ''),
'current_sort': self.request.GET.get('sort', 'newest'),
'date_from': self.request.GET.get('date_from', ''),
'date_to': self.request.GET.get('date_to', ''),
'featured_only': self.request.GET.get('featured') == 'true',
})
# Add filter options
context.update({
'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],
'sort_options': [
('newest', 'Newest First'),
('oldest', 'Oldest First'),
('title', 'Title A-Z'),
('author', 'Author A-Z'),
('popular', 'Most Popular'),
('comments', 'Most Commented'),
],
})
return context
def get_paginate_by(self, queryset):
"""Dynamic pagination"""
per_page = self.request.GET.get('per_page')
if per_page:
try:
per_page = int(per_page)
return min(max(per_page, 5), 100) # Between 5 and 100
except ValueError:
pass
return self.paginate_by
class AjaxPostListView(ListView):
"""AJAX-enabled post list"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(status='published').select_related('author')
def render_to_response(self, context, **response_kwargs):
"""Handle AJAX requests differently"""
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# Return JSON for AJAX requests
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.isoformat(),
'url': post.get_absolute_url(),
})
return JsonResponse({
'posts': posts_data,
'has_next': context['page_obj'].has_next() if context.get('page_obj') else False,
'has_previous': context['page_obj'].has_previous() if context.get('page_obj') else False,
'current_page': context['page_obj'].number if context.get('page_obj') else 1,
'total_pages': context['paginator'].num_pages if context.get('paginator') else 1,
})
# Regular template response
return super().render_to_response(context, **response_kwargs)
from django.views.generic import DetailView
from django.shortcuts import get_object_or_404
from django.http import Http404
class PostDetailView(DetailView):
"""Basic post detail view"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
"""Only show published posts"""
return Post.objects.filter(status='published').select_related(
'author', 'category'
).prefetch_related('tags')
def get_object(self, queryset=None):
"""Get object and increment view count"""
obj = super().get_object(queryset)
# Increment view count (avoid race conditions)
Post.objects.filter(pk=obj.pk).update(views=F('views') + 1)
return obj
class PostDetailBySlugView(DetailView):
"""Post detail view using slug"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
slug_field = 'slug'
slug_url_kwarg = 'slug'
def get_queryset(self):
return Post.objects.filter(status='published').select_related(
'author', 'category'
).prefetch_related('tags', 'comments__author')
class UserPostDetailView(DetailView):
"""Post detail with author verification"""
model = Post
template_name = 'blog/user_post_detail.html'
context_object_name = 'post'
def get_queryset(self):
"""Filter by author from URL"""
author_username = self.kwargs['username']
return Post.objects.filter(
author__username=author_username,
status='published'
).select_related('author', 'category')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add author information
context['author'] = self.object.author
# Add other posts by same author
context['other_posts'] = Post.objects.filter(
author=self.object.author,
status='published'
).exclude(pk=self.object.pk)[:5]
return context
class AdvancedPostDetailView(DetailView):
"""Advanced post detail with comments and related content"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.filter(status='published').select_related(
'author', 'category'
).prefetch_related(
'tags',
'comments__author',
'comments__replies__author'
)
def get_object(self, queryset=None):
"""Get object with additional processing"""
obj = super().get_object(queryset)
# Check if user can view this post
if not self.can_view_post(obj):
raise Http404("Post not found")
# Track view (with session-based deduplication)
self.track_view(obj)
return obj
def can_view_post(self, post):
"""Check if user can view this post"""
# Always allow published posts
if post.status == 'published':
return True
# Allow author to view their own posts
if post.author == self.request.user:
return True
# Allow staff to view any post
if self.request.user.is_staff:
return True
return False
def track_view(self, post):
"""Track post view with session deduplication"""
session_key = f'viewed_post_{post.pk}'
if not self.request.session.get(session_key):
# Increment view count
Post.objects.filter(pk=post.pk).update(views=F('views') + 1)
# Mark as viewed in session
self.request.session[session_key] = True
# Create view log entry
PostView.objects.create(
post=post,
user=self.request.user if self.request.user.is_authenticated else None,
ip_address=self.get_client_ip(),
user_agent=self.request.META.get('HTTP_USER_AGENT', ''),
timestamp=timezone.now()
)
def get_client_ip(self):
"""Get client IP address"""
x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0]
return self.request.META.get('REMOTE_ADDR')
def get_context_data(self, **kwargs):
"""Add comprehensive context data"""
context = super().get_context_data(**kwargs)
post = self.object
# Add comments (approved only for non-staff)
if self.request.user.is_staff:
comments = post.comments.all()
else:
comments = post.comments.filter(approved=True)
context['comments'] = comments.select_related('author').order_by('created_at')
# Add comment form for authenticated users
if self.request.user.is_authenticated:
context['comment_form'] = CommentForm()
# Add related posts
context['related_posts'] = self.get_related_posts(post)
# Add navigation (previous/next posts)
context.update(self.get_post_navigation(post))
# Add reading time estimate
context['reading_time'] = self.calculate_reading_time(post.content)
# Add social sharing data
context['sharing_data'] = self.get_sharing_data(post)
return context
def get_related_posts(self, post):
"""Get related posts based on category and tags"""
related_posts = Post.objects.filter(
status='published'
).exclude(pk=post.pk)
# Posts in same category
category_posts = related_posts.filter(category=post.category)[:3]
# Posts with similar tags
tag_posts = related_posts.filter(
tags__in=post.tags.all()
).distinct()[:3]
# Combine and deduplicate
related_ids = set()
final_related = []
for post_list in [category_posts, tag_posts]:
for related_post in post_list:
if related_post.id not in related_ids:
related_ids.add(related_post.id)
final_related.append(related_post)
if len(final_related) >= 6:
break
if len(final_related) >= 6:
break
return final_related
def get_post_navigation(self, post):
"""Get previous and next posts"""
try:
previous_post = Post.objects.filter(
created_at__lt=post.created_at,
status='published'
).order_by('-created_at').first()
except Post.DoesNotExist:
previous_post = None
try:
next_post = Post.objects.filter(
created_at__gt=post.created_at,
status='published'
).order_by('created_at').first()
except Post.DoesNotExist:
next_post = None
return {
'previous_post': previous_post,
'next_post': next_post,
}
def calculate_reading_time(self, content):
"""Calculate estimated reading time"""
words_per_minute = 200
word_count = len(content.split())
reading_time = max(1, round(word_count / words_per_minute))
return reading_time
def get_sharing_data(self, post):
"""Get social sharing data"""
return {
'title': post.title,
'description': post.excerpt or post.content[:160],
'url': self.request.build_absolute_uri(post.get_absolute_url()),
'image': post.featured_image.url if post.featured_image else None,
'author': post.author.get_full_name() or post.author.username,
}
class MultiFormatDetailView(DetailView):
"""Detail view supporting multiple output formats"""
model = Post
def get_template_names(self):
"""Select template based on format"""
format_param = self.request.GET.get('format', 'html')
if format_param == 'print':
return ['blog/post_detail_print.html']
elif format_param == 'mobile':
return ['blog/post_detail_mobile.html']
elif format_param == 'amp':
return ['blog/post_detail_amp.html']
return ['blog/post_detail.html']
def render_to_response(self, context, **response_kwargs):
"""Handle different response formats"""
format_param = self.request.GET.get('format', 'html')
if format_param == 'json':
# Return JSON representation
post = context['post']
data = {
'id': post.id,
'title': post.title,
'content': post.content,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
'tags': [tag.name for tag in post.tags.all()],
}
return JsonResponse(data)
elif format_param == 'xml':
# Return XML representation
post = context['post']
xml_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<post>
<id>{post.id}</id>
<title><![CDATA[{post.title}]]></title>
<content><![CDATA[{post.content}]]></content>
<author>{post.author.username}</author>
<created_at>{post.created_at.isoformat()}</created_at>
</post>'''
return HttpResponse(xml_content, content_type='application/xml')
# Default HTML response
return super().render_to_response(context, **response_kwargs)
from django.views.generic.dates import (
ArchiveIndexView, YearArchiveView, MonthArchiveView,
DayArchiveView, DateDetailView
)
class PostArchiveIndexView(ArchiveIndexView):
"""Archive index showing latest posts"""
model = Post
date_field = 'created_at'
template_name = 'blog/archive_index.html'
context_object_name = 'posts'
paginate_by = 20
allow_empty = True
def get_queryset(self):
return Post.objects.filter(status='published').select_related('author')
class PostYearArchiveView(YearArchiveView):
"""Posts for a specific year"""
model = Post
date_field = 'created_at'
template_name = 'blog/year_archive.html'
context_object_name = 'posts'
make_object_list = True
allow_empty = True
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
year = self.get_year()
# Add year statistics
context['year_stats'] = {
'total_posts': context['posts'].count(),
'months_with_posts': context['posts'].dates('created_at', 'month'),
}
return context
class PostMonthArchiveView(MonthArchiveView):
"""Posts for a specific month"""
model = Post
date_field = 'created_at'
template_name = 'blog/month_archive.html'
context_object_name = 'posts'
month_format = '%m'
allow_empty = True
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add month navigation
current_date = datetime.date(int(self.get_year()), int(self.get_month()), 1)
# Previous month
if current_date.month == 1:
prev_month = current_date.replace(year=current_date.year - 1, month=12)
else:
prev_month = current_date.replace(month=current_date.month - 1)
# Next month
if current_date.month == 12:
next_month = current_date.replace(year=current_date.year + 1, month=1)
else:
next_month = current_date.replace(month=current_date.month + 1)
context.update({
'previous_month': prev_month,
'next_month': next_month,
'current_month': current_date,
})
return context
class PostDayArchiveView(DayArchiveView):
"""Posts for a specific day"""
model = Post
date_field = 'created_at'
template_name = 'blog/day_archive.html'
context_object_name = 'posts'
month_format = '%m'
allow_empty = True
class PostDateDetailView(DateDetailView):
"""Single post by date and slug"""
model = Post
date_field = 'created_at'
template_name = 'blog/post_detail.html'
context_object_name = 'post'
month_format = '%m'
slug_field = 'slug'
class CustomArchiveView(TemplateView):
"""Custom archive with calendar widget"""
template_name = 'blog/custom_archive.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get archive data
context.update({
'archive_dates': self.get_archive_dates(),
'popular_posts': self.get_popular_posts(),
'recent_posts': self.get_recent_posts(),
'calendar_data': self.get_calendar_data(),
})
return context
def get_archive_dates(self):
"""Get dates with post counts"""
return Post.objects.filter(
status='published'
).extra(
select={'year': 'EXTRACT(year FROM created_at)',
'month': 'EXTRACT(month FROM created_at)'}
).values('year', 'month').annotate(
post_count=Count('id')
).order_by('-year', '-month')
def get_popular_posts(self):
"""Get most popular posts from archive"""
return Post.objects.filter(
status='published'
).order_by('-views')[:10]
def get_recent_posts(self):
"""Get recent posts"""
return Post.objects.filter(
status='published'
).order_by('-created_at')[:10]
def get_calendar_data(self):
"""Get calendar data for current month"""
import calendar
from datetime import date
today = date.today()
cal = calendar.monthcalendar(today.year, today.month)
# Get posts for current month
month_posts = Post.objects.filter(
status='published',
created_at__year=today.year,
created_at__month=today.month
).values('created_at__day').annotate(
post_count=Count('id')
)
# Create lookup dict
posts_by_day = {item['created_at__day']: item['post_count']
for item in month_posts}
# Add post counts to calendar
calendar_with_posts = []
for week in cal:
week_with_posts = []
for day in week:
if day == 0:
week_with_posts.append({'day': 0, 'posts': 0})
else:
week_with_posts.append({
'day': day,
'posts': posts_by_day.get(day, 0)
})
calendar_with_posts.append(week_with_posts)
return {
'calendar': calendar_with_posts,
'month_name': calendar.month_name[today.month],
'year': today.year,
}
Built-in generic views provide powerful abstractions for common patterns. ListView handles object collections with filtering, sorting, and pagination. DetailView manages single object display with related data. Archive views offer sophisticated date-based browsing capabilities. Understanding these views and their customization options enables rapid development of feature-rich Django applications.
Common Base Classes
Django provides several base classes that form the foundation of all class-based views. Understanding these base classes is essential for effectively using and customizing CBVs in your applications.
Views for CRUD Operations
Django's generic views provide comprehensive support for Create, Read, Update, and Delete (CRUD) operations. These views handle the common patterns of object lifecycle management with minimal code while maintaining flexibility for customization.