Mixins are a powerful feature of Django's class-based views that enable code reuse through multiple inheritance. They provide a way to compose functionality from different sources, creating flexible and maintainable view hierarchies.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, DetailView, CreateView
from django.urls import reverse_lazy
class PostListView(LoginRequiredMixin, ListView):
"""Post list requiring authentication"""
model = Post
template_name = 'blog/post_list.html'
login_url = '/accounts/login/' # Custom login URL
redirect_field_name = 'next' # Custom redirect parameter name
class UserPostListView(LoginRequiredMixin, ListView):
"""User's own posts"""
model = Post
template_name = 'blog/user_posts.html'
def get_queryset(self):
"""Filter posts by current user"""
return Post.objects.filter(author=self.request.user)
class ProfileView(LoginRequiredMixin, DetailView):
"""User profile view"""
model = User
template_name = 'accounts/profile.html'
context_object_name = 'profile_user'
def get_object(self):
"""Return current user's profile"""
return self.request.user
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import PermissionDenied
class PostCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""Create post with permission check"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
permission_required = 'blog.add_post'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostEditView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""Edit post with multiple permissions"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
permission_required = ['blog.change_post', 'blog.view_post']
def get_queryset(self):
"""Additional filtering beyond permissions"""
return Post.objects.filter(author=self.request.user)
class AdminPostView(PermissionRequiredMixin, ListView):
"""Admin-only post management"""
model = Post
template_name = 'admin/post_management.html'
permission_required = 'blog.change_post'
raise_exception = True # Raise 403 instead of redirecting
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['pending_posts'] = Post.objects.filter(status='pending')
return context
from django.contrib.auth.mixins import UserPassesTestMixin
class AuthorOnlyMixin(UserPassesTestMixin):
"""Mixin to restrict access to post authors"""
def test_func(self):
"""Test if user is the post author"""
post = self.get_object()
return post.author == self.request.user
class PostEditView(LoginRequiredMixin, AuthorOnlyMixin, UpdateView):
"""Edit post - author only"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
class StaffOrAuthorMixin(UserPassesTestMixin):
"""Allow staff or post author"""
def test_func(self):
post = self.get_object()
return (
self.request.user.is_staff or
post.author == self.request.user
)
class PostDeleteView(LoginRequiredMixin, StaffOrAuthorMixin, DeleteView):
"""Delete post - staff or author"""
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:post_list')
class PremiumUserMixin(UserPassesTestMixin):
"""Restrict to premium users"""
def test_func(self):
return (
self.request.user.is_authenticated and
hasattr(self.request.user, 'profile') and
self.request.user.profile.is_premium
)
class PremiumContentView(PremiumUserMixin, DetailView):
"""Premium content view"""
model = PremiumContent
template_name = 'premium/content_detail.html'
def handle_no_permission(self):
"""Custom handling for non-premium users"""
messages.info(
self.request,
'This content requires a premium subscription.'
)
return redirect('accounts:upgrade')
class AjaxResponseMixin:
"""Mixin to handle AJAX requests"""
def dispatch(self, request, *args, **kwargs):
"""Check if request is AJAX"""
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'error': 'AJAX request required'}, status=400)
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
"""Return JSON response for valid forms"""
response = super().form_valid(form)
return JsonResponse({
'success': True,
'redirect_url': self.get_success_url(),
'message': getattr(self, 'success_message', 'Operation completed successfully')
})
def form_invalid(self, form):
"""Return JSON response for invalid forms"""
return JsonResponse({
'success': False,
'errors': form.errors,
'non_field_errors': form.non_field_errors()
})
class TimestampMixin:
"""Mixin to add timestamp information to context"""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['current_timestamp'] = timezone.now()
context['server_time'] = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
return context
class PaginationMixin:
"""Enhanced pagination mixin"""
paginate_by = 20
paginate_orphans = 3
def get_paginate_by(self, queryset):
"""Allow 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
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if 'page_obj' in context:
page_obj = context['page_obj']
# Add pagination info
context.update({
'pagination_info': {
'current_page': page_obj.number,
'total_pages': page_obj.paginator.num_pages,
'total_items': page_obj.paginator.count,
'items_per_page': page_obj.paginator.per_page,
'start_index': page_obj.start_index(),
'end_index': page_obj.end_index(),
},
'page_range': self.get_page_range(page_obj),
})
return context
def get_page_range(self, page_obj):
"""Get smart page range for pagination"""
current_page = page_obj.number
total_pages = page_obj.paginator.num_pages
# Show 5 pages around current page
start_page = max(1, current_page - 2)
end_page = min(total_pages, current_page + 2)
return range(start_page, end_page + 1)
class SearchMixin:
"""Mixin to add search functionality"""
search_fields = [] # Override in subclass
def get_queryset(self):
queryset = super().get_queryset()
query = self.request.GET.get('q')
if query and self.search_fields:
from django.db.models import Q
search_query = Q()
for field in self.search_fields:
search_query |= Q(**{f'{field}__icontains': query})
queryset = queryset.filter(search_query)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('q', '')
return context
class FilterMixin:
"""Mixin to add filtering functionality"""
filter_fields = {} # {'field_name': 'url_param_name'}
def get_queryset(self):
queryset = super().get_queryset()
for field, param in self.filter_fields.items():
value = self.request.GET.get(param)
if value:
queryset = queryset.filter(**{field: value})
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add current filters to context
current_filters = {}
for field, param in self.filter_fields.items():
value = self.request.GET.get(param)
if value:
current_filters[param] = value
context['current_filters'] = current_filters
return context
class OwnershipMixin:
"""Mixin to handle object ownership"""
ownership_field = 'user' # Field that defines ownership
def get_queryset(self):
"""Filter by ownership"""
queryset = super().get_queryset()
if self.request.user.is_authenticated:
filter_kwargs = {self.ownership_field: self.request.user}
return queryset.filter(**filter_kwargs)
return queryset.none()
def form_valid(self, form):
"""Set owner on form save"""
if hasattr(form, 'instance'):
setattr(form.instance, self.ownership_field, self.request.user)
return super().form_valid(form)
class SoftDeleteMixin:
"""Mixin for soft delete functionality"""
def get_queryset(self):
"""Exclude soft-deleted objects"""
queryset = super().get_queryset()
return queryset.filter(deleted_at__isnull=True)
def delete(self, request, *args, **kwargs):
"""Soft delete instead of hard delete"""
self.object = self.get_object()
# Mark as deleted
self.object.deleted_at = timezone.now()
self.object.deleted_by = request.user
self.object.save()
return HttpResponseRedirect(self.get_success_url())
class AuditMixin:
"""Mixin to add audit trail functionality"""
def form_valid(self, form):
"""Add audit information"""
if hasattr(form, 'instance'):
instance = form.instance
if not instance.pk: # New object
instance.created_by = self.request.user
instance.created_at = timezone.now()
instance.updated_by = self.request.user
instance.updated_at = timezone.now()
return super().form_valid(form)
class CacheMixin:
"""Mixin to add caching functionality"""
cache_timeout = 300 # 5 minutes default
def get_cache_key(self):
"""Generate cache key for this view"""
return f"{self.__class__.__name__}:{self.request.path}:{self.request.GET.urlencode()}"
def dispatch(self, request, *args, **kwargs):
"""Check cache before processing"""
if request.method == 'GET':
cache_key = self.get_cache_key()
cached_response = cache.get(cache_key)
if cached_response:
return cached_response
response = super().dispatch(request, *args, **kwargs)
# Cache GET responses
if request.method == 'GET' and response.status_code == 200:
cache_key = self.get_cache_key()
cache.set(cache_key, response, self.cache_timeout)
return response
class RateLimitMixin:
"""Mixin to add rate limiting"""
rate_limit = 60 # requests per hour
def dispatch(self, request, *args, **kwargs):
"""Check rate limit before processing"""
if self.is_rate_limited(request):
return JsonResponse({
'error': 'Rate limit exceeded. Please try again later.'
}, status=429)
return super().dispatch(request, *args, **kwargs)
def is_rate_limited(self, request):
"""Check if request is rate limited"""
# Create cache key based on user or IP
if request.user.is_authenticated:
cache_key = f"rate_limit:user:{request.user.id}:{self.__class__.__name__}"
else:
ip = self.get_client_ip(request)
cache_key = f"rate_limit:ip:{ip}:{self.__class__.__name__}"
# Get current request count
current_requests = cache.get(cache_key, 0)
if current_requests >= self.rate_limit:
return True
# Increment counter
cache.set(cache_key, current_requests + 1, 3600) # 1 hour
return False
def get_client_ip(self, request):
"""Get client IP address"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0]
return request.META.get('REMOTE_ADDR')
class PostListView(
LoginRequiredMixin,
PaginationMixin,
SearchMixin,
FilterMixin,
CacheMixin,
ListView
):
"""Advanced post list with multiple mixins"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
# SearchMixin configuration
search_fields = ['title', 'content', 'author__username']
# FilterMixin configuration
filter_fields = {
'category__slug': 'category',
'status': 'status',
'author__username': 'author',
}
# CacheMixin configuration
cache_timeout = 600 # 10 minutes
def get_queryset(self):
"""Base queryset with optimizations"""
return Post.objects.select_related('author', 'category').prefetch_related('tags')
class PostCreateView(
LoginRequiredMixin,
PermissionRequiredMixin,
AjaxResponseMixin,
AuditMixin,
SuccessMessageMixin,
CreateView
):
"""Advanced post creation with multiple mixins"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
permission_required = 'blog.add_post'
success_message = "Post created successfully!"
def get_success_url(self):
return self.object.get_absolute_url()
class PostUpdateView(
LoginRequiredMixin,
AuthorOnlyMixin,
AjaxResponseMixin,
AuditMixin,
RateLimitMixin,
UpdateView
):
"""Advanced post update with security and rate limiting"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
rate_limit = 30 # 30 updates per hour
def get_success_url(self):
return self.object.get_absolute_url()
class BaseSecureMixin(LoginRequiredMixin, RateLimitMixin):
"""Base mixin for secure views"""
rate_limit = 100 # Default rate limit
def dispatch(self, request, *args, **kwargs):
"""Add security headers"""
response = super().dispatch(request, *args, **kwargs)
# Add security headers
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = 'DENY'
response['X-XSS-Protection'] = '1; mode=block'
return response
class BaseContentMixin(BaseSecureMixin, AuditMixin, SoftDeleteMixin):
"""Base mixin for content management"""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add common content management context
context.update({
'can_edit': self.can_edit_object(),
'can_delete': self.can_delete_object(),
'content_stats': self.get_content_stats(),
})
return context
def can_edit_object(self):
"""Check if user can edit this object"""
if hasattr(self, 'object') and self.object:
return (
self.object.created_by == self.request.user or
self.request.user.is_staff
)
return False
def can_delete_object(self):
"""Check if user can delete this object"""
return self.can_edit_object() # Same logic for now
def get_content_stats(self):
"""Get content statistics"""
if self.request.user.is_authenticated:
return {
'user_content_count': self.model.objects.filter(
created_by=self.request.user
).count(),
'total_content_count': self.model.objects.count(),
}
return {}
class BlogMixin(BaseContentMixin, SearchMixin, FilterMixin):
"""Specialized mixin for blog views"""
# SearchMixin configuration
search_fields = ['title', 'content']
# FilterMixin configuration
filter_fields = {
'category__slug': 'category',
'status': 'status',
'featured': 'featured',
}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Add blog-specific context
context.update({
'categories': Category.objects.filter(active=True),
'recent_posts': Post.objects.filter(
status='published'
).order_by('-created_at')[:5],
'popular_tags': Tag.objects.annotate(
post_count=Count('posts')
).order_by('-post_count')[:10],
})
return context
# Usage of custom mixin hierarchy
class PostListView(BlogMixin, PaginationMixin, ListView):
"""Blog post list with full functionality"""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
class PostDetailView(BlogMixin, DetailView):
"""Blog post detail with full functionality"""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
class PostCreateView(BlogMixin, CreateView):
"""Blog post creation with full functionality"""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
# tests/test_mixins.py
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User, AnonymousUser
from django.core.exceptions import PermissionDenied
from django.http import Http404
class MixinTestCase(TestCase):
"""Base test case for mixin testing"""
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass'
)
self.staff_user = User.objects.create_user(
username='staffuser',
email='staff@example.com',
password='testpass',
is_staff=True
)
class AuthenticationMixinTests(MixinTestCase):
"""Test authentication mixins"""
def test_login_required_mixin_authenticated(self):
"""Test LoginRequiredMixin with authenticated user"""
request = self.factory.get('/test/')
request.user = self.user
view = PostListView()
view.setup(request)
# Should not raise exception
response = view.dispatch(request)
self.assertEqual(response.status_code, 200)
def test_login_required_mixin_anonymous(self):
"""Test LoginRequiredMixin with anonymous user"""
request = self.factory.get('/test/')
request.user = AnonymousUser()
view = PostListView()
view.setup(request)
# Should redirect to login
response = view.dispatch(request)
self.assertEqual(response.status_code, 302)
def test_permission_required_mixin(self):
"""Test PermissionRequiredMixin"""
# Add permission to user
from django.contrib.auth.models import Permission
permission = Permission.objects.get(codename='add_post')
self.user.user_permissions.add(permission)
request = self.factory.get('/test/')
request.user = self.user
view = PostCreateView()
view.setup(request)
# Should allow access
response = view.dispatch(request)
self.assertEqual(response.status_code, 200)
class CustomMixinTests(MixinTestCase):
"""Test custom mixins"""
def test_ajax_response_mixin(self):
"""Test AjaxResponseMixin"""
# Non-AJAX request
request = self.factory.get('/test/')
request.user = self.user
view = AjaxOnlyView()
view.setup(request)
response = view.dispatch(request)
self.assertEqual(response.status_code, 400)
# AJAX request
request = self.factory.get('/test/', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
request.user = self.user
view = AjaxOnlyView()
view.setup(request)
response = view.dispatch(request)
self.assertEqual(response.status_code, 200)
def test_ownership_mixin(self):
"""Test OwnershipMixin"""
# Create test post
post = Post.objects.create(
title='Test Post',
content='Test content',
author=self.user
)
request = self.factory.get('/test/')
request.user = self.user
view = UserPostListView()
view.setup(request)
queryset = view.get_queryset()
self.assertIn(post, queryset)
# Different user shouldn't see the post
other_user = User.objects.create_user('other', 'other@example.com', 'pass')
request.user = other_user
view = UserPostListView()
view.setup(request)
queryset = view.get_queryset()
self.assertNotIn(post, queryset)
def test_rate_limit_mixin(self):
"""Test RateLimitMixin"""
request = self.factory.get('/test/')
request.user = self.user
view = RateLimitedView()
view.rate_limit = 2 # Very low limit for testing
view.setup(request)
# First request should work
response = view.dispatch(request)
self.assertEqual(response.status_code, 200)
# Second request should work
response = view.dispatch(request)
self.assertEqual(response.status_code, 200)
# Third request should be rate limited
response = view.dispatch(request)
self.assertEqual(response.status_code, 429)
Mixins provide a powerful way to compose functionality in Django class-based views. They enable code reuse, separation of concerns, and flexible view hierarchies. Understanding both built-in and custom mixins allows you to build maintainable, feature-rich applications with minimal code duplication.
Handling Forms with Class-Based Views
Django's class-based views provide powerful abstractions for form handling, from simple contact forms to complex multi-step workflows. Understanding form views and their integration patterns is essential for building interactive web applications.
URL Configuration with Class-Based Views
Configuring URLs for class-based views requires understanding the as_view() method, parameter passing, and URL pattern organization. This chapter covers comprehensive URL configuration strategies for CBVs.