URLs and Views

View Decorators

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.

View Decorators

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.

Built-in Django Decorators

Authentication Decorators

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')

HTTP Method Decorators

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})

CSRF Protection Decorators

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)

Caching Decorators

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')

Custom Decorators

Basic Custom Decorators

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()
    }

Advanced Custom Decorators

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')

Decorator Factories

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': []})

Decorator Composition and Best Practices

Proper Decorator Ordering

# 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)
                )
            )
        )
    )
)

Reusable Decorator Combinations

# 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')

Testing Decorated Views

# 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.