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.
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')
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})
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")
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')
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')
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))
# 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},
}
# 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.
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.
Handling HTTP Methods
HTTP methods define the type of action being performed on a resource. Django provides comprehensive support for handling different HTTP methods, enabling you to build RESTful APIs and implement proper request handling patterns.