Class-based views (CBVs) provide an object-oriented approach to handling HTTP requests in Django. They leverage Python's class inheritance to create reusable, composable view components that reduce code duplication and improve maintainability.
from django.views.generic import View
from django.http import HttpResponse
from django.shortcuts import render
class BasicView(View):
"""Most basic class-based view"""
def get(self, request, *args, **kwargs):
"""Handle GET requests"""
return HttpResponse("Hello from a class-based view!")
def post(self, request, *args, **kwargs):
"""Handle POST requests"""
return HttpResponse("POST request received!")
# URL configuration
from django.urls import path
urlpatterns = [
path('basic/', BasicView.as_view(), name='basic_view'),
]
CBVs automatically route HTTP methods to corresponding class methods:
class BlogView(View):
"""Demonstrate HTTP method dispatch"""
def get(self, request, *args, **kwargs):
"""Handle GET requests - display blog posts"""
posts = Post.objects.filter(status='published')
return render(request, 'blog/post_list.html', {'posts': posts})
def post(self, request, *args, **kwargs):
"""Handle POST requests - create new post"""
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect('blog:post_detail', pk=post.pk)
# If form is invalid, redisplay with errors
posts = Post.objects.filter(status='published')
return render(request, 'blog/post_list.html', {
'posts': posts,
'form': form
})
def put(self, request, *args, **kwargs):
"""Handle PUT requests - update existing post"""
# PUT logic here
return HttpResponse("PUT request handled")
def delete(self, request, *args, **kwargs):
"""Handle DELETE requests - remove post"""
# DELETE logic here
return HttpResponse("DELETE request handled")
# Equivalent function-based view
def blog_view_fbv(request):
"""Function-based equivalent"""
if request.method == 'GET':
posts = Post.objects.filter(status='published')
return render(request, 'blog/post_list.html', {'posts': posts})
elif request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect('blog:post_detail', pk=post.pk)
posts = Post.objects.filter(status='published')
return render(request, 'blog/post_list.html', {
'posts': posts,
'form': form
})
elif request.method == 'PUT':
return HttpResponse("PUT request handled")
elif request.method == 'DELETE':
return HttpResponse("DELETE request handled")
class PostListView(ListView):
"""Demonstrate class attributes vs method overrides"""
# Class attributes (declarative approach)
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
ordering = ['-created_at']
# Method overrides (programmatic approach)
def get_queryset(self):
"""Override to add custom filtering"""
queryset = super().get_queryset()
# Filter by status
queryset = queryset.filter(status='published')
# Add search functionality
search_query = self.request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(content__icontains=search_query)
)
return queryset
def get_context_data(self, **kwargs):
"""Add extra context data"""
context = super().get_context_data(**kwargs)
# Add search query to context
context['search_query'] = self.request.GET.get('search', '')
# Add categories for sidebar
context['categories'] = Category.objects.all()
# Add recent posts
context['recent_posts'] = Post.objects.filter(
status='published'
).order_by('-created_at')[:5]
return context
def get_paginate_by(self, queryset):
"""Dynamic pagination based on user preference"""
# Allow users to choose page size
per_page = self.request.GET.get('per_page', self.paginate_by)
try:
per_page = int(per_page)
# Limit to reasonable range
return min(max(per_page, 5), 50)
except (ValueError, TypeError):
return self.paginate_by
class DetailedView(View):
"""Demonstrate CBV request processing lifecycle"""
def setup(self, request, *args, **kwargs):
"""Initialize view instance"""
print("1. setup() - Initialize view")
super().setup(request, *args, **kwargs)
# Custom initialization
self.user_agent = request.META.get('HTTP_USER_AGENT', '')
self.is_mobile = 'mobile' in self.user_agent.lower()
def dispatch(self, request, *args, **kwargs):
"""Route request to appropriate method"""
print("2. dispatch() - Route to method")
# Custom dispatch logic
if not request.user.is_authenticated and request.method == 'POST':
return redirect('accounts:login')
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""Handle GET request"""
print("3. get() - Handle GET request")
context = {
'is_mobile': self.is_mobile,
'user_agent': self.user_agent,
}
return render(request, 'example/detail.html', context)
def post(self, request, *args, **kwargs):
"""Handle POST request"""
print("3. post() - Handle POST request")
# Process form data
return HttpResponse("POST processed")
# Method execution order:
# 1. setup()
# 2. dispatch()
# 3. get()/post()/put()/delete() etc.
class PostDetailView(DetailView):
"""Demonstrate generic view lifecycle"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def setup(self, request, *args, **kwargs):
"""1. Initialize view"""
print("1. setup()")
super().setup(request, *args, **kwargs)
def dispatch(self, request, *args, **kwargs):
"""2. Route request"""
print("2. dispatch()")
return super().dispatch(request, *args, **kwargs)
def get_object(self, queryset=None):
"""3. Retrieve object"""
print("3. get_object()")
obj = super().get_object(queryset)
# Increment view count
obj.views += 1
obj.save(update_fields=['views'])
return obj
def get_context_data(self, **kwargs):
"""4. Build context"""
print("4. get_context_data()")
context = super().get_context_data(**kwargs)
# Add related posts
context['related_posts'] = Post.objects.filter(
category=self.object.category
).exclude(pk=self.object.pk)[:3]
return context
def get_template_names(self):
"""5. Determine template"""
print("5. get_template_names()")
# Custom template selection logic
if self.request.user.is_staff:
return ['blog/post_detail_admin.html']
return super().get_template_names()
def render_to_response(self, context, **response_kwargs):
"""6. Render response"""
print("6. render_to_response()")
return super().render_to_response(context, **response_kwargs)
# Generic view method execution order:
# 1. setup()
# 2. dispatch()
# 3. get_object() (for detail views)
# 4. get_context_data()
# 5. get_template_names()
# 6. render_to_response()
class PostListView(ListView):
"""Configure view through class attributes"""
# Model configuration
model = Post
queryset = Post.objects.select_related('author', 'category')
# Template configuration
template_name = 'blog/post_list.html'
context_object_name = 'posts'
# Pagination configuration
paginate_by = 12
paginate_orphans = 3
page_kwarg = 'page'
# Ordering configuration
ordering = ['-created_at', 'title']
# Additional configuration
allow_empty = True
extra_context = {
'page_title': 'Blog Posts',
'show_sidebar': True,
}
class CustomPostListView(ListView):
"""Customize view through method overrides"""
model = Post
template_name = 'blog/post_list.html'
def get_queryset(self):
"""Custom queryset with filtering"""
queryset = super().get_queryset()
# Filter by category if provided
category_slug = self.kwargs.get('category_slug')
if category_slug:
queryset = queryset.filter(category__slug=category_slug)
# Filter by author if provided
author_id = self.request.GET.get('author')
if author_id:
queryset = queryset.filter(author_id=author_id)
# Filter by date range
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)
return queryset.filter(status='published')
def get_template_names(self):
"""Dynamic template selection"""
# Use different template for AJAX requests
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return ['blog/post_list_ajax.html']
# Use mobile template for mobile devices
user_agent = self.request.META.get('HTTP_USER_AGENT', '').lower()
if any(device in user_agent for device in ['mobile', 'android', 'iphone']):
return ['blog/post_list_mobile.html']
return super().get_template_names()
def get_context_data(self, **kwargs):
"""Add custom context data"""
context = super().get_context_data(**kwargs)
# Add filter information
context.update({
'category_slug': self.kwargs.get('category_slug'),
'selected_author': self.request.GET.get('author'),
'date_from': self.request.GET.get('date_from'),
'date_to': self.request.GET.get('date_to'),
})
# Add sidebar data
context.update({
'categories': Category.objects.annotate(
post_count=Count('posts', filter=Q(posts__status='published'))
),
'authors': User.objects.filter(
posts__status='published'
).annotate(
post_count=Count('posts')
).distinct(),
'archive_dates': Post.objects.filter(
status='published'
).dates('created_at', 'month', order='DESC')[:12],
})
return context
def get_paginate_by(self, queryset):
"""Dynamic pagination"""
# Allow URL parameter to override 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 super().get_paginate_by(queryset)
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
class AuthorRequiredMixin(UserPassesTestMixin):
"""Mixin to require post author or staff"""
def test_func(self):
obj = self.get_object()
return (
obj.author == self.request.user or
self.request.user.is_staff
)
class PostUpdateView(LoginRequiredMixin, AuthorRequiredMixin,
SuccessMessageMixin, UpdateView):
"""Compose functionality through mixins"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_message = "Post updated successfully!"
def get_success_url(self):
return reverse('blog:post_detail', kwargs={'pk': self.object.pk})
class AjaxResponseMixin:
"""Mixin to handle AJAX requests"""
def dispatch(self, request, *args, **kwargs):
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return HttpResponseBadRequest("AJAX request required")
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
response = super().form_valid(form)
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'redirect_url': self.get_success_url()
})
return response
def form_invalid(self, form):
response = super().form_invalid(form)
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': False,
'errors': form.errors
})
return response
class AjaxPostCreateView(LoginRequiredMixin, AjaxResponseMixin, CreateView):
"""AJAX-enabled post creation"""
model = Post
form_class = PostForm
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
import json
@method_decorator(csrf_exempt, name='dispatch')
class PostAPIView(View):
"""API-style view handling multiple HTTP methods"""
def get(self, request, pk=None):
"""Retrieve post(s)"""
if pk:
# Single post
try:
post = Post.objects.get(pk=pk, status='published')
data = {
'id': post.id,
'title': post.title,
'content': post.content,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
}
return JsonResponse(data)
except Post.DoesNotExist:
return JsonResponse({'error': 'Post not found'}, status=404)
else:
# List of posts
posts = Post.objects.filter(status='published')[:20]
data = {
'posts': [
{
'id': post.id,
'title': post.title,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
}
for post in posts
]
}
return JsonResponse(data)
def post(self, request):
"""Create new post"""
try:
data = json.loads(request.body)
post = Post.objects.create(
title=data['title'],
content=data['content'],
author=request.user,
status='published'
)
return JsonResponse({
'id': post.id,
'title': post.title,
'created_at': post.created_at.isoformat(),
}, status=201)
except (KeyError, json.JSONDecodeError) as e:
return JsonResponse({'error': str(e)}, status=400)
def put(self, request, pk):
"""Update existing post"""
try:
post = Post.objects.get(pk=pk)
data = json.loads(request.body)
post.title = data.get('title', post.title)
post.content = data.get('content', post.content)
post.save()
return JsonResponse({
'id': post.id,
'title': post.title,
'updated_at': post.updated_at.isoformat(),
})
except Post.DoesNotExist:
return JsonResponse({'error': 'Post not found'}, status=404)
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
def delete(self, request, pk):
"""Delete post"""
try:
post = Post.objects.get(pk=pk)
post.delete()
return JsonResponse({'message': 'Post deleted'}, status=204)
except Post.DoesNotExist:
return JsonResponse({'error': 'Post not found'}, status=404)
class MultiStepFormView(View):
"""Handle multi-step form workflow"""
def get(self, request):
"""Display current step"""
step = request.session.get('form_step', 1)
if step == 1:
form = StepOneForm()
template = 'forms/step_one.html'
elif step == 2:
form = StepTwoForm()
template = 'forms/step_two.html'
elif step == 3:
form = StepThreeForm()
template = 'forms/step_three.html'
else:
# Invalid step, reset
request.session['form_step'] = 1
return redirect('multi_step_form')
context = {
'form': form,
'step': step,
'total_steps': 3,
}
return render(request, template, context)
def post(self, request):
"""Process current step"""
step = request.session.get('form_step', 1)
if step == 1:
form = StepOneForm(request.POST)
if form.is_valid():
request.session['step_one_data'] = form.cleaned_data
request.session['form_step'] = 2
return redirect('multi_step_form')
elif step == 2:
form = StepTwoForm(request.POST)
if form.is_valid():
request.session['step_two_data'] = form.cleaned_data
request.session['form_step'] = 3
return redirect('multi_step_form')
elif step == 3:
form = StepThreeForm(request.POST)
if form.is_valid():
# Combine all step data
step_one_data = request.session.get('step_one_data', {})
step_two_data = request.session.get('step_two_data', {})
step_three_data = form.cleaned_data
# Process complete form
self.process_complete_form(
step_one_data, step_two_data, step_three_data
)
# Clear session data
for key in ['form_step', 'step_one_data', 'step_two_data']:
request.session.pop(key, None)
messages.success(request, 'Form submitted successfully!')
return redirect('form_success')
# Form invalid, redisplay current step
template = f'forms/step_{step}.html'
context = {
'form': form,
'step': step,
'total_steps': 3,
}
return render(request, template, context)
def process_complete_form(self, step_one, step_two, step_three):
"""Process the complete multi-step form data"""
# Combine and process all form data
complete_data = {**step_one, **step_two, **step_three}
# Create database records, send emails, etc.
# Implementation depends on your specific needs
pass
Class-based views provide a powerful foundation for building Django applications. Understanding the method dispatch system, lifecycle, and composition patterns enables you to create maintainable, reusable view components that handle complex requirements efficiently.
Class Based Views
Class-based views (CBVs) provide a powerful, object-oriented approach to handling HTTP requests in Django. They offer reusability, inheritance, and built-in functionality that can significantly reduce code duplication while maintaining flexibility for complex requirements.
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.