View decorators are a powerful way to modify view behavior without changing the view function itself. Django provides built-in decorators for common tasks and supports custom decorators for specialized functionality.
from django.contrib.auth.decorators import login_required, user_passes_test, permission_required
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render, redirect
from django.contrib import messages
@login_required
def profile_view(request):
"""View requiring user authentication"""
return render(request, 'accounts/profile.html', {
'user': request.user
})
@login_required(login_url='/custom-login/')
def custom_login_redirect(request):
"""Custom login URL for unauthenticated users"""
return render(request, 'dashboard.html')
@login_required(redirect_field_name='next')
def dashboard_view(request):
"""Custom redirect field name"""
return render(request, 'dashboard.html')
# Permission-based access control
@permission_required('blog.add_post')
def create_post(request):
"""Requires specific permission"""
# Only users with 'blog.add_post' permission can access
return render(request, 'blog/create_post.html')
@permission_required('blog.change_post', raise_exception=True)
def edit_post(request, pk):
"""Raise 403 instead of redirecting to login"""
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/edit_post.html', {'post': post})
@permission_required(['blog.add_post', 'blog.change_post'])
def manage_posts(request):
"""Requires multiple permissions (AND logic)"""
return render(request, 'blog/manage_posts.html')
# Custom user tests
def is_author(user):
"""Custom user test function"""
return user.is_authenticated and hasattr(user, 'profile') and user.profile.is_author
def is_premium_user(user):
"""Check if user has premium subscription"""
return user.is_authenticated and getattr(user, 'is_premium', False)
@user_passes_test(is_author)
def author_dashboard(request):
"""Only authors can access"""
return render(request, 'blog/author_dashboard.html')
@user_passes_test(is_premium_user, login_url='/upgrade/')
def premium_content(request):
"""Premium users only with custom redirect"""
return render(request, 'premium/content.html')
@staff_member_required
def admin_stats(request):
"""Staff members only"""
return render(request, 'admin/stats.html')
from django.views.decorators.http import (
require_http_methods, require_GET, require_POST,
require_safe, condition
)
from django.views.decorators.csrf import csrf_exempt, csrf_protect
import hashlib
from datetime import datetime
@require_GET
def api_get_posts(request):
"""Only allow GET requests"""
posts = Post.objects.all()
return JsonResponse([{
'id': post.id,
'title': post.title,
'created_at': post.created_at.isoformat()
} for post in posts], safe=False)
@require_POST
def api_create_post(request):
"""Only allow POST requests"""
# Handle POST data
return JsonResponse({'status': 'created'})
@require_http_methods(["GET", "POST"])
def post_form_view(request):
"""Allow only GET and POST methods"""
if request.method == 'GET':
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
elif request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save()
return redirect('blog:detail', pk=post.pk)
return render(request, 'blog/post_form.html', {'form': form})
@require_safe # Only GET and HEAD methods
def safe_view(request):
"""Read-only view that doesn't modify data"""
return render(request, 'blog/read_only.html')
# Conditional processing based on ETag and Last-Modified
def generate_etag(request, *args, **kwargs):
"""Generate ETag for caching"""
content = f"{request.path}{request.user.id if request.user.is_authenticated else 'anonymous'}"
return hashlib.md5(content.encode()).hexdigest()
def get_last_modified(request, *args, **kwargs):
"""Get last modified time"""
try:
latest_post = Post.objects.latest('updated_at')
return latest_post.updated_at
except Post.DoesNotExist:
return datetime.now()
@condition(etag_func=generate_etag, last_modified_func=get_last_modified)
def cached_post_list(request):
"""View with conditional caching"""
posts = Post.objects.all()
return render(request, 'blog/post_list.html', {'posts': posts})
from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_cookie
from django.middleware.csrf import get_token
import json
@csrf_exempt
def api_webhook(request):
"""External webhook endpoint (CSRF exempt)"""
if request.method == 'POST':
try:
data = json.loads(request.body)
# Process webhook data
return JsonResponse({'status': 'received'})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
return JsonResponse({'error': 'POST required'}, status=405)
@ensure_csrf_cookie
def get_csrf_token(request):
"""Ensure CSRF cookie is set for AJAX requests"""
return JsonResponse({
'csrf_token': get_token(request)
})
@csrf_protect
def protected_api_view(request):
"""Explicitly protect view even if middleware is disabled"""
if request.method == 'POST':
# Process protected data
return JsonResponse({'status': 'success'})
return JsonResponse({'error': 'POST required'}, status=405)
from django.views.decorators.cache import cache_page, cache_control, never_cache
from django.views.decorators.vary import vary_on_headers, vary_on_cookie
from django.core.cache import cache
from django.utils.cache import patch_cache_control
@cache_page(60 * 15) # Cache for 15 minutes
def cached_post_list(request):
"""Cached post list view"""
posts = Post.objects.select_related('author').all()
return render(request, 'blog/post_list.html', {'posts': posts})
@cache_page(60 * 60, key_prefix='user_posts') # 1 hour with custom prefix
@vary_on_cookie
def user_posts(request):
"""Cache varies by user cookie"""
if request.user.is_authenticated:
posts = Post.objects.filter(author=request.user)
else:
posts = Post.objects.none()
return render(request, 'blog/user_posts.html', {'posts': posts})
@cache_control(max_age=3600, must_revalidate=True)
def static_content(request):
"""Set cache control headers"""
return render(request, 'pages/static_content.html')
@never_cache
def dynamic_content(request):
"""Never cache this view"""
current_time = timezone.now()
return render(request, 'pages/dynamic.html', {'current_time': current_time})
@vary_on_headers('User-Agent', 'Accept-Language')
def browser_specific_view(request):
"""Cache varies by browser and language"""
user_agent = request.META.get('HTTP_USER_AGENT', '')
language = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
return render(request, 'pages/browser_specific.html', {
'user_agent': user_agent,
'language': language
})
# Custom cache key function
def custom_cache_key(request, *args, **kwargs):
"""Generate custom cache key"""
user_id = request.user.id if request.user.is_authenticated else 'anonymous'
return f'custom_view:{user_id}:{request.path}'
@cache_page(60 * 30, key_prefix=custom_cache_key)
def custom_cached_view(request):
"""View with custom cache key"""
return render(request, 'pages/custom_cached.html')
from functools import wraps
from django.http import JsonResponse, HttpResponseForbidden
from django.shortcuts import redirect
from django.contrib import messages
import time
import logging
logger = logging.getLogger(__name__)
def timing_decorator(view_func):
"""Measure and log view execution time"""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
start_time = time.time()
response = view_func(request, *args, **kwargs)
end_time = time.time()
execution_time = end_time - start_time
# Log execution time
logger.info(f'{view_func.__name__} executed in {execution_time:.3f}s')
# Add header to response
if hasattr(response, '__setitem__'):
response['X-Execution-Time'] = f'{execution_time:.3f}s'
return response
return wrapper
def ajax_required(view_func):
"""Require AJAX requests"""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
if request.method == 'GET':
messages.error(request, 'This endpoint requires AJAX.')
return redirect('home')
else:
return JsonResponse({'error': 'AJAX request required'}, status=400)
return view_func(request, *args, **kwargs)
return wrapper
def json_response(view_func):
"""Automatically convert dict returns to JSON"""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)
if isinstance(response, dict):
return JsonResponse(response)
return response
return wrapper
# Usage examples
@timing_decorator
@ajax_required
@json_response
def api_get_user_data(request):
"""API endpoint with multiple decorators"""
return {
'user': {
'id': request.user.id,
'username': request.user.username,
'email': request.user.email,
},
'timestamp': timezone.now().isoformat()
}
from django.core.cache import cache
from django.http import HttpResponse
import hashlib
import pickle
def rate_limit(requests_per_minute=60, block_duration=300):
"""Rate limiting decorator with configurable limits"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
# Create unique key for this client
client_ip = request.META.get('REMOTE_ADDR', 'unknown')
cache_key = f'rate_limit:{client_ip}:{view_func.__name__}'
# Check if client is blocked
block_key = f'blocked:{client_ip}:{view_func.__name__}'
if cache.get(block_key):
return JsonResponse({
'error': 'Rate limit exceeded. Please try again later.',
'retry_after': block_duration
}, status=429)
# Get current request count
current_requests = cache.get(cache_key, 0)
if current_requests >= requests_per_minute:
# Block the client
cache.set(block_key, True, block_duration)
return JsonResponse({
'error': 'Rate limit exceeded. You have been temporarily blocked.',
'retry_after': block_duration
}, status=429)
# Increment request count
cache.set(cache_key, current_requests + 1, 60)
return view_func(request, *args, **kwargs)
return wrapper
return decorator
def cache_result(timeout=300, key_func=None):
"""Cache view results with custom key function"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
# Generate cache key
if key_func:
cache_key = key_func(request, *args, **kwargs)
else:
# Default key generation
key_parts = [
view_func.__name__,
request.path,
str(request.user.id if request.user.is_authenticated else 'anonymous'),
hashlib.md5(str(sorted(request.GET.items())).encode()).hexdigest()
]
cache_key = ':'.join(key_parts)
# Try to get cached result
cached_result = cache.get(cache_key)
if cached_result is not None:
return pickle.loads(cached_result)
# Execute view and cache result
response = view_func(request, *args, **kwargs)
# Only cache successful responses
if hasattr(response, 'status_code') and response.status_code == 200:
cache.set(cache_key, pickle.dumps(response), timeout)
return response
return wrapper
return decorator
def require_subscription(subscription_type='premium'):
"""Require specific subscription level"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
messages.error(request, 'Please log in to access this content.')
return redirect('accounts:login')
user_subscription = getattr(request.user, 'subscription_type', 'free')
subscription_levels = {
'free': 0,
'basic': 1,
'premium': 2,
'enterprise': 3
}
required_level = subscription_levels.get(subscription_type, 0)
user_level = subscription_levels.get(user_subscription, 0)
if user_level < required_level:
messages.error(request, f'This content requires a {subscription_type} subscription.')
return redirect('accounts:upgrade')
return view_func(request, *args, **kwargs)
return wrapper
return decorator
# Usage with custom key function
def user_specific_cache_key(request, *args, **kwargs):
"""Generate user-specific cache key"""
return f'user_dashboard:{request.user.id}:{request.path}'
@require_subscription('premium')
@cache_result(timeout=600, key_func=user_specific_cache_key)
def premium_dashboard(request):
"""Premium user dashboard with caching"""
# Expensive operations here
return render(request, 'premium/dashboard.html')
def permission_required_any(*permissions):
"""Require any of the specified permissions (OR logic)"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect('accounts:login')
if not any(request.user.has_perm(perm) for perm in permissions):
return HttpResponseForbidden('Insufficient permissions')
return view_func(request, *args, **kwargs)
return wrapper
return decorator
def log_access(log_level=logging.INFO, include_data=False):
"""Log view access with configurable options"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
# Prepare log data
log_data = {
'view': view_func.__name__,
'user': request.user.username if request.user.is_authenticated else 'anonymous',
'ip': request.META.get('REMOTE_ADDR'),
'method': request.method,
'path': request.path,
}
if include_data:
log_data.update({
'get_params': dict(request.GET),
'post_data': dict(request.POST) if request.method == 'POST' else None,
})
# Log access
logger.log(log_level, f'View access: {view_func.__name__}', extra=log_data)
return view_func(request, *args, **kwargs)
return wrapper
return decorator
def validate_params(**param_validators):
"""Validate request parameters"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
errors = {}
for param_name, validator in param_validators.items():
param_value = request.GET.get(param_name) or request.POST.get(param_name)
try:
if param_value is not None:
validator(param_value)
except ValueError as e:
errors[param_name] = str(e)
if errors:
return JsonResponse({'errors': errors}, status=400)
return view_func(request, *args, **kwargs)
return wrapper
return decorator
# Validator functions
def validate_positive_int(value):
"""Validate positive integer"""
try:
int_value = int(value)
if int_value <= 0:
raise ValueError('Must be a positive integer')
return int_value
except (ValueError, TypeError):
raise ValueError('Must be a valid integer')
def validate_email(value):
"""Validate email format"""
from django.core.validators import validate_email as django_validate_email
try:
django_validate_email(value)
return value
except ValidationError:
raise ValueError('Must be a valid email address')
# Usage examples
@permission_required_any('blog.add_post', 'blog.change_post', 'blog.delete_post')
def manage_posts(request):
"""Requires any blog management permission"""
return render(request, 'blog/manage.html')
@log_access(log_level=logging.WARNING, include_data=True)
def sensitive_view(request):
"""Log access to sensitive view with full data"""
return render(request, 'sensitive/data.html')
@validate_params(
page=validate_positive_int,
email=validate_email
)
def search_users(request):
"""Validate search parameters"""
page = int(request.GET.get('page', 1))
email = request.GET.get('email')
# Search logic here
return JsonResponse({'results': []})
# Correct order: outer to inner execution
@cache_page(300) # 1. Cache the final response
@vary_on_cookie # 2. Vary cache by cookie
@login_required # 3. Check authentication
@permission_required('blog.add_post') # 4. Check permissions
@require_http_methods(['GET', 'POST']) # 5. Validate HTTP method
@timing_decorator # 6. Measure execution time
def create_post(request):
"""Properly ordered decorators"""
# View logic here
pass
# Alternative syntax for complex decorator stacks
def create_post_view(request):
"""View function without decorators"""
# View logic here
pass
# Apply decorators programmatically
create_post = cache_page(300)(
vary_on_cookie(
login_required(
permission_required('blog.add_post')(
require_http_methods(['GET', 'POST'])(
timing_decorator(create_post_view)
)
)
)
)
)
# Create reusable decorator combinations
def api_view_decorators(methods=['GET'], permissions=None, rate_limit_rpm=60):
"""Combine common API view decorators"""
def decorator(view_func):
# Apply decorators in reverse order
decorated_view = view_func
# Rate limiting
decorated_view = rate_limit(rate_limit_rpm)(decorated_view)
# AJAX requirement
decorated_view = ajax_required(decorated_view)
# JSON response conversion
decorated_view = json_response(decorated_view)
# HTTP methods
decorated_view = require_http_methods(methods)(decorated_view)
# Permissions
if permissions:
if isinstance(permissions, (list, tuple)):
decorated_view = permission_required_any(*permissions)(decorated_view)
else:
decorated_view = permission_required(permissions)(decorated_view)
# Authentication
decorated_view = login_required(decorated_view)
return decorated_view
return decorator
def admin_view_decorators(view_func):
"""Standard decorators for admin views"""
return staff_member_required(
timing_decorator(
log_access(log_level=logging.INFO)(view_func)
)
)
# Usage
@api_view_decorators(methods=['POST'], permissions='blog.add_post', rate_limit_rpm=30)
def api_create_post(request):
"""API endpoint with standard decorators"""
return {'status': 'created'}
@admin_view_decorators
def admin_dashboard(request):
"""Admin view with standard decorators"""
return render(request, 'admin/dashboard.html')
# tests/test_decorators.py
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType
from unittest.mock import patch
from blog.models import Post
from blog.views import create_post
class DecoratorTests(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass'
)
# Add permission
content_type = ContentType.objects.get_for_model(Post)
permission = Permission.objects.get(
codename='add_post',
content_type=content_type
)
self.user.user_permissions.add(permission)
def test_login_required_decorator(self):
"""Test login_required decorator"""
request = self.factory.get('/create/')
request.user = self.user
response = create_post(request)
self.assertEqual(response.status_code, 200)
# Test unauthenticated user
from django.contrib.auth.models import AnonymousUser
request.user = AnonymousUser()
response = create_post(request)
self.assertEqual(response.status_code, 302) # Redirect to login
def test_permission_required_decorator(self):
"""Test permission_required decorator"""
request = self.factory.get('/create/')
request.user = self.user
response = create_post(request)
self.assertEqual(response.status_code, 200)
# Remove permission
self.user.user_permissions.clear()
response = create_post(request)
self.assertEqual(response.status_code, 302) # Redirect to login
@patch('blog.views.cache')
def test_rate_limit_decorator(self, mock_cache):
"""Test rate limiting decorator"""
# Mock cache to simulate rate limit exceeded
mock_cache.get.return_value = 60 # Max requests reached
request = self.factory.post('/api/create/')
request.user = self.user
response = api_create_post(request)
self.assertEqual(response.status_code, 429)
def test_ajax_required_decorator(self):
"""Test AJAX requirement decorator"""
# Regular request
request = self.factory.post('/api/create/')
request.user = self.user
response = api_create_post(request)
self.assertEqual(response.status_code, 400)
# AJAX request
request = self.factory.post('/api/create/',
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
request.user = self.user
response = api_create_post(request)
self.assertNotEqual(response.status_code, 400)
View decorators provide a clean, reusable way to add functionality to views. They promote code reuse, separation of concerns, and make it easy to apply consistent behavior across multiple views. Understanding both built-in and custom decorators is essential for building maintainable Django applications.
Writing Function-Based Views
Function-based views (FBVs) are the foundation of Django's view system. They provide direct, explicit control over request handling and are ideal for custom logic, simple operations, and learning Django concepts.
Rendering Responses
Django views must return HttpResponse objects or subclasses. Understanding different response types and rendering techniques is crucial for building flexible, maintainable web applications.