URLs and Views

Redirects

Redirects are essential for guiding users through your application, handling URL changes, and implementing proper navigation flows. Django provides multiple ways to handle redirects with different HTTP status codes and use cases.

Redirects

Redirects are essential for guiding users through your application, handling URL changes, and implementing proper navigation flows. Django provides multiple ways to handle redirects with different HTTP status codes and use cases.

Basic Redirect Types

HTTP Redirect Status Codes

from django.shortcuts import redirect
from django.http import (
    HttpResponseRedirect, HttpResponsePermanentRedirect,
    HttpResponseNotModified
)
from django.urls import reverse, reverse_lazy

def temporary_redirect_302(request):
    """Temporary redirect (302) - default behavior"""
    # Using redirect() shortcut (returns 302 by default)
    return redirect('blog:post_list')

def permanent_redirect_301(request):
    """Permanent redirect (301) - for SEO and caching"""
    return redirect('blog:post_list', permanent=True)

def redirect_with_status_code(request):
    """Explicit redirect with custom status code"""
    # 302 Temporary redirect
    return HttpResponseRedirect(reverse('blog:post_list'))
    
    # 301 Permanent redirect
    # return HttpResponsePermanentRedirect(reverse('blog:post_list'))

def see_other_redirect_303(request):
    """303 See Other - after POST to prevent duplicate submissions"""
    if request.method == 'POST':
        # Process form data
        # ...
        
        # Redirect to GET endpoint to prevent resubmission
        response = HttpResponseRedirect(reverse('blog:post_list'))
        response.status_code = 303
        return response
    
    return render(request, 'blog/form.html')

def not_modified_304(request):
    """304 Not Modified - for caching"""
    # Check if content has changed
    last_modified = get_last_modified_time()
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
    
    if if_modified_since and not content_changed_since(if_modified_since):
        return HttpResponseNotModified()
    
    # Return normal response if content changed
    return render(request, 'blog/content.html')

Redirect Patterns

from django.contrib import messages
from django.contrib.auth.decorators import login_required

def redirect_to_url(request):
    """Redirect to absolute URL"""
    return redirect('https://example.com/external-page/')

def redirect_to_view_name(request):
    """Redirect using view name"""
    return redirect('blog:post_list')

def redirect_with_arguments(request, post_id):
    """Redirect with URL arguments"""
    return redirect('blog:post_detail', pk=post_id)

def redirect_with_query_params(request):
    """Redirect with query parameters"""
    # Method 1: Build URL manually
    base_url = reverse('blog:post_list')
    query_params = '?category=tech&page=2'
    return redirect(base_url + query_params)
    
    # Method 2: Using redirect with query string
    from django.http import QueryDict
    query_dict = QueryDict(mutable=True)
    query_dict.update({'category': 'tech', 'page': 2})
    
    url = reverse('blog:post_list') + '?' + query_dict.urlencode()
    return redirect(url)

def conditional_redirect(request):
    """Redirect based on conditions"""
    if not request.user.is_authenticated:
        # Redirect to login with next parameter
        login_url = reverse('accounts:login')
        next_url = request.get_full_path()
        return redirect(f'{login_url}?next={next_url}')
    
    if request.user.is_staff:
        return redirect('admin:index')
    
    if hasattr(request.user, 'profile') and request.user.profile.is_premium:
        return redirect('premium:dashboard')
    
    # Default redirect
    return redirect('accounts:profile')

@login_required
def post_form_redirect(request):
    """Redirect after form processing"""
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            
            # Success message and redirect
            messages.success(request, 'Post created successfully!')
            return redirect('blog:post_detail', pk=post.pk)
        else:
            # Form has errors, don't redirect
            messages.error(request, 'Please correct the errors below.')
    else:
        form = PostForm()
    
    return render(request, 'blog/post_form.html', {'form': form})

Advanced Redirect Patterns

URL Canonicalization

from django.utils.text import slugify
from django.db.models import Q

def canonical_post_url(request, pk, slug=None):
    """Ensure canonical URL for posts"""
    post = get_object_or_404(Post, pk=pk, status='published')
    
    # Check if slug matches
    if slug != post.slug:
        # Redirect to canonical URL
        return redirect('blog:post_detail_canonical', pk=pk, slug=post.slug, permanent=True)
    
    return render(request, 'blog/post_detail.html', {'post': post})

def handle_old_urls(request, old_id):
    """Handle legacy URL structure"""
    try:
        # Try to find post by old ID mapping
        post_mapping = PostURLMapping.objects.get(old_id=old_id)
        return redirect('blog:post_detail', pk=post_mapping.post.pk, permanent=True)
    except PostURLMapping.DoesNotExist:
        # Try to find by old ID directly
        try:
            post = Post.objects.get(legacy_id=old_id)
            return redirect('blog:post_detail', pk=post.pk, permanent=True)
        except Post.DoesNotExist:
            # Return 404 if no mapping found
            raise Http404("Post not found")

def normalize_category_url(request, category_name):
    """Normalize category URLs"""
    # Convert to proper slug format
    normalized_slug = slugify(category_name.lower())
    
    try:
        category = Category.objects.get(
            Q(slug=normalized_slug) | Q(name__iexact=category_name)
        )
        
        # Redirect if URL doesn't match canonical slug
        if category_name != category.slug:
            return redirect('blog:category_posts', slug=category.slug, permanent=True)
        
        posts = Post.objects.filter(category=category, status='published')
        return render(request, 'blog/category_posts.html', {
            'category': category,
            'posts': posts
        })
        
    except Category.DoesNotExist:
        raise Http404("Category not found")

Authentication and Permission Redirects

from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth import login
from django.contrib import messages

def login_redirect_view(request):
    """Handle login redirects"""
    if request.user.is_authenticated:
        # User already logged in, redirect to appropriate page
        next_url = request.GET.get('next')
        
        if next_url:
            # Validate next URL to prevent open redirects
            if is_safe_url(next_url, allowed_hosts={request.get_host()}):
                return redirect(next_url)
        
        # Default redirect for authenticated users
        if request.user.is_staff:
            return redirect('admin:index')
        else:
            return redirect('accounts:profile')
    
    # Show login form
    return render(request, 'registration/login.html')

def is_safe_url(url, allowed_hosts):
    """Check if redirect URL is safe"""
    from urllib.parse import urlparse
    
    if not url:
        return False
    
    parsed = urlparse(url)
    
    # Only allow relative URLs or URLs from allowed hosts
    if parsed.netloc and parsed.netloc not in allowed_hosts:
        return False
    
    return True

@login_required
def premium_content_redirect(request):
    """Redirect based on subscription status"""
    if not hasattr(request.user, 'subscription'):
        messages.info(request, 'Please choose a subscription plan to access premium content.')
        return redirect('accounts:subscription_plans')
    
    if not request.user.subscription.is_active:
        messages.warning(request, 'Your subscription has expired. Please renew to continue.')
        return redirect('accounts:renew_subscription')
    
    if request.user.subscription.plan != 'premium':
        messages.info(request, 'This content requires a premium subscription.')
        return redirect('accounts:upgrade_subscription')
    
    # User has valid premium subscription
    return render(request, 'premium/content.html')

def age_verification_redirect(request):
    """Age verification redirect"""
    if not request.session.get('age_verified'):
        return redirect('accounts:age_verification')
    
    return render(request, 'content/age_restricted.html')

def maintenance_mode_redirect(request):
    """Redirect during maintenance"""
    if settings.MAINTENANCE_MODE:
        # Allow staff to bypass maintenance mode
        if not (request.user.is_authenticated and request.user.is_staff):
            return render(request, 'maintenance.html', status=503)
    
    return render(request, 'home.html')

Form Processing Redirects

from django.contrib import messages
from django.urls import reverse_lazy

def contact_form_view(request):
    """Contact form with proper redirect handling"""
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            # Process form
            send_contact_email(form.cleaned_data)
            
            # Success message and redirect
            messages.success(request, 'Thank you for your message! We\'ll get back to you soon.')
            
            # Redirect to prevent form resubmission
            return redirect('contact:success')
        else:
            # Form has errors, show them
            messages.error(request, 'Please correct the errors below.')
    else:
        form = ContactForm()
    
    return render(request, 'contact/form.html', {'form': form})

def multi_step_form_redirect(request):
    """Multi-step form with session-based redirects"""
    step = request.session.get('form_step', 1)
    
    if step == 1:
        if request.method == 'POST':
            form = StepOneForm(request.POST)
            if form.is_valid():
                # Save step 1 data to session
                request.session['step1_data'] = form.cleaned_data
                request.session['form_step'] = 2
                return redirect('forms:multi_step')
        else:
            form = StepOneForm()
        
        return render(request, 'forms/step1.html', {'form': form})
    
    elif step == 2:
        if request.method == 'POST':
            form = StepTwoForm(request.POST)
            if form.is_valid():
                # Combine data from both steps
                step1_data = request.session.get('step1_data', {})
                step2_data = form.cleaned_data
                
                # Process complete form
                process_multi_step_form(step1_data, step2_data)
                
                # Clear session data
                request.session.pop('step1_data', None)
                request.session.pop('form_step', None)
                
                messages.success(request, 'Form submitted successfully!')
                return redirect('forms:success')
        else:
            form = StepTwoForm()
        
        return render(request, 'forms/step2.html', {'form': form})
    
    else:
        # Invalid step, reset
        request.session.pop('form_step', None)
        return redirect('forms:multi_step')

def ajax_form_redirect(request):
    """Handle AJAX form submissions with redirects"""
    if request.method == 'POST':
        form = AjaxForm(request.POST)
        
        if form.is_valid():
            # Process form
            result = process_ajax_form(form.cleaned_data)
            
            # Return JSON response with redirect URL
            return JsonResponse({
                'success': True,
                'message': 'Form submitted successfully!',
                'redirect_url': reverse('forms:success')
            })
        else:
            # Return form errors
            return JsonResponse({
                'success': False,
                'errors': form.errors
            })
    
    return render(request, 'forms/ajax_form.html')

Redirect Utilities and Helpers

Custom Redirect Functions

from django.shortcuts import redirect
from django.urls import reverse
from django.http import QueryDict
from urllib.parse import urlencode

def redirect_with_params(view_name, *args, **kwargs):
    """Redirect with query parameters"""
    # Separate URL kwargs from query params
    url_kwargs = {}
    query_params = {}
    
    for key, value in kwargs.items():
        if key.startswith('q_'):
            # Query parameter (remove q_ prefix)
            query_params[key[2:]] = value
        else:
            # URL parameter
            url_kwargs[key] = value
    
    # Build URL
    url = reverse(view_name, args=args, kwargs=url_kwargs)
    
    if query_params:
        url += '?' + urlencode(query_params)
    
    return redirect(url)

def smart_redirect(request, fallback_url='home'):
    """Smart redirect with fallback"""
    # Try 'next' parameter first
    next_url = request.GET.get('next') or request.POST.get('next')
    
    if next_url and is_safe_url(next_url, allowed_hosts={request.get_host()}):
        return redirect(next_url)
    
    # Try HTTP_REFERER
    referer = request.META.get('HTTP_REFERER')
    if referer and is_safe_url(referer, allowed_hosts={request.get_host()}):
        return redirect(referer)
    
    # Fallback to default URL
    return redirect(fallback_url)

def redirect_with_message(view_name, message, level=messages.INFO, *args, **kwargs):
    """Redirect with flash message"""
    messages.add_message(request, level, message)
    return redirect(view_name, *args, **kwargs)

class RedirectBuilder:
    """Fluent interface for building redirects"""
    
    def __init__(self, view_name):
        self.view_name = view_name
        self.args = []
        self.kwargs = {}
        self.query_params = {}
        self.permanent = False
        self.message = None
        self.message_level = messages.INFO
    
    def arg(self, *args):
        self.args.extend(args)
        return self
    
    def kwarg(self, **kwargs):
        self.kwargs.update(kwargs)
        return self
    
    def query(self, **params):
        self.query_params.update(params)
        return self
    
    def make_permanent(self):
        self.permanent = True
        return self
    
    def with_message(self, message, level=messages.INFO):
        self.message = message
        self.message_level = level
        return self
    
    def build(self, request=None):
        # Build URL
        url = reverse(self.view_name, args=self.args, kwargs=self.kwargs)
        
        if self.query_params:
            url += '?' + urlencode(self.query_params)
        
        # Add message if provided
        if self.message and request:
            messages.add_message(request, self.message_level, self.message)
        
        return redirect(url, permanent=self.permanent)

# Usage examples
def example_redirect_usage(request):
    # Simple redirect with parameters
    return redirect_with_params('blog:post_list', q_category='tech', q_page=2)
    
    # Smart redirect
    return smart_redirect(request, fallback_url='blog:post_list')
    
    # Fluent redirect builder
    return (RedirectBuilder('blog:post_detail')
            .kwarg(pk=123)
            .query(ref='homepage')
            .with_message('Welcome back!', messages.SUCCESS)
            .build(request))

Redirect Middleware

# middleware/redirect_middleware.py
from django.shortcuts import redirect
from django.urls import reverse
from django.conf import settings
import re

class RedirectMiddleware:
    """Custom redirect middleware"""
    
    def __init__(self, get_response):
        self.get_response = get_response
        
        # Compile redirect patterns
        self.redirects = getattr(settings, 'CUSTOM_REDIRECTS', {})
        self.compiled_patterns = {}
        
        for pattern, target in self.redirects.items():
            self.compiled_patterns[re.compile(pattern)] = target
    
    def __call__(self, request):
        # Check for custom redirects before processing view
        for pattern, target in self.compiled_patterns.items():
            if pattern.match(request.path):
                # Handle different target types
                if callable(target):
                    return target(request)
                elif isinstance(target, dict):
                    return redirect(target['url'], permanent=target.get('permanent', False))
                else:
                    return redirect(target)
        
        response = self.get_response(request)
        
        # Post-process response if needed
        return response

class WWWRedirectMiddleware:
    """Force www subdomain"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        host = request.get_host().lower()
        
        if not host.startswith('www.') and not settings.DEBUG:
            # Redirect to www version
            new_url = f"https://www.{host}{request.get_full_path()}"
            return redirect(new_url, permanent=True)
        
        return self.get_response(request)

class TrailingSlashRedirectMiddleware:
    """Custom trailing slash handling"""
    
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        # Add trailing slash to specific patterns
        if (request.path.startswith('/api/') and 
            not request.path.endswith('/') and 
            '.' not in request.path.split('/')[-1]):
            
            return redirect(request.path + '/', permanent=True)
        
        return self.get_response(request)

# settings.py configuration
MIDDLEWARE = [
    'middleware.redirect_middleware.WWWRedirectMiddleware',
    'middleware.redirect_middleware.RedirectMiddleware',
    'middleware.redirect_middleware.TrailingSlashRedirectMiddleware',
    # ... other middleware
]

CUSTOM_REDIRECTS = {
    r'^/old-blog/(.+)/$': '/blog/\\1/',
    r'^/legacy/posts/(\d+)/$': lambda request: redirect('blog:post_detail', pk=request.path.split('/')[-2]),
    r'^/deprecated/$': {'url': '/new-page/', 'permanent': True},
}

Testing Redirects

# tests/test_redirects.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User

class RedirectTests(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass'
        )
    
    def test_login_required_redirect(self):
        """Test redirect to login for protected views"""
        response = self.client.get('/protected/')
        
        self.assertEqual(response.status_code, 302)
        self.assertIn('/accounts/login/', response.url)
        self.assertIn('next=', response.url)
    
    def test_post_login_redirect(self):
        """Test redirect after successful login"""
        response = self.client.post('/accounts/login/', {
            'username': 'testuser',
            'password': 'testpass',
            'next': '/dashboard/'
        })
        
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, '/dashboard/')
    
    def test_canonical_url_redirect(self):
        """Test canonical URL redirects"""
        post = Post.objects.create(
            title='Test Post',
            slug='test-post',
            content='Test content',
            author=self.user
        )
        
        # Wrong slug should redirect
        response = self.client.get(f'/blog/{post.pk}/wrong-slug/')
        
        self.assertEqual(response.status_code, 301)  # Permanent redirect
        self.assertEqual(response.url, f'/blog/{post.pk}/test-post/')
    
    def test_form_submission_redirect(self):
        """Test redirect after form submission"""
        self.client.login(username='testuser', password='testpass')
        
        response = self.client.post('/blog/create/', {
            'title': 'New Post',
            'content': 'Post content'
        })
        
        self.assertEqual(response.status_code, 302)
        self.assertTrue(response.url.startswith('/blog/'))
    
    def test_safe_redirect_validation(self):
        """Test that unsafe redirects are blocked"""
        response = self.client.get('/login/?next=https://evil.com/')
        
        # Should not redirect to external site
        self.assertNotIn('evil.com', response.url)
    
    def test_middleware_redirects(self):
        """Test custom middleware redirects"""
        response = self.client.get('/old-blog/some-post/')
        
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.url, '/blog/some-post/')

Redirects are crucial for user experience, SEO, and application flow. Understanding different redirect types, implementing proper redirect patterns, and handling edge cases ensures your Django application provides smooth navigation and maintains good search engine rankings.