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.
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, JsonResponse, Http404
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from .models import Post, Category
from .forms import PostForm
def post_list(request):
"""Display a list of published posts"""
posts = Post.objects.filter(status='published').order_by('-created_at')
# Handle search
query = request.GET.get('q')
if query:
posts = posts.filter(title__icontains=query)
# Handle category filtering
category_slug = request.GET.get('category')
if category_slug:
category = get_object_or_404(Category, slug=category_slug)
posts = posts.filter(category=category)
context = {
'posts': posts,
'query': query,
'category_slug': category_slug,
}
return render(request, 'blog/post_list.html', context)
def post_detail(request, pk):
"""Display a single post"""
post = get_object_or_404(Post, pk=pk, status='published')
# Increment view count
post.views += 1
post.save(update_fields=['views'])
# Get related posts
related_posts = Post.objects.filter(
category=post.category,
status='published'
).exclude(pk=post.pk)[:3]
context = {
'post': post,
'related_posts': related_posts,
}
return render(request, 'blog/post_detail.html', context)
@login_required
def post_create(request):
"""Create a new post"""
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m() # Save many-to-many relationships
return redirect('blog:detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form})
The request object contains all information about the current HTTP request:
def analyze_request(request):
"""Demonstrate request object properties"""
# HTTP method
method = request.method # GET, POST, PUT, DELETE, etc.
# URL information
path = request.path # '/blog/posts/'
full_path = request.get_full_path() # '/blog/posts/?q=django'
host = request.get_host() # 'example.com'
scheme = request.scheme # 'http' or 'https'
# Request data
get_params = request.GET # QueryDict of GET parameters
post_data = request.POST # QueryDict of POST data
files = request.FILES # MultiValueDict of uploaded files
# Headers
headers = request.headers # Case-insensitive dict-like object
user_agent = request.headers.get('User-Agent')
content_type = request.content_type
# User and session
user = request.user # User object or AnonymousUser
session = request.session # Session dictionary
# Raw request body (for API endpoints)
body = request.body # bytes
# Meta information
remote_addr = request.META.get('REMOTE_ADDR') # Client IP
server_name = request.META.get('SERVER_NAME')
context = {
'method': method,
'path': path,
'get_params': dict(get_params),
'user_agent': user_agent,
'is_authenticated': user.is_authenticated,
}
return render(request, 'debug/request_info.html', context)
Django views can return various types of responses:
from django.http import (
HttpResponse, JsonResponse, HttpResponseRedirect,
HttpResponsePermanentRedirect, HttpResponseNotFound,
HttpResponseBadRequest, HttpResponseForbidden,
HttpResponseServerError, StreamingHttpResponse,
FileResponse
)
from django.template.response import TemplateResponse
import json
import csv
import io
def html_response(request):
"""Return HTML response"""
return HttpResponse('<h1>Hello, World!</h1>', content_type='text/html')
def json_response(request):
"""Return JSON response"""
data = {
'message': 'Hello, World!',
'timestamp': timezone.now().isoformat(),
'user': request.user.username if request.user.is_authenticated else None
}
return JsonResponse(data)
def json_list_response(request):
"""Return JSON list (requires safe=False)"""
posts = Post.objects.values('id', 'title', 'created_at')
return JsonResponse(list(posts), safe=False)
def csv_response(request):
"""Return CSV file response"""
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="posts.csv"'
writer = csv.writer(response)
writer.writerow(['ID', 'Title', 'Author', 'Created'])
for post in Post.objects.all():
writer.writerow([
post.id,
post.title,
post.author.username,
post.created_at.strftime('%Y-%m-%d')
])
return response
def pdf_response(request):
"""Return PDF response (requires reportlab)"""
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
buffer = io.BytesIO()
p = canvas.Canvas(buffer, pagesize=letter)
# Draw content
p.drawString(100, 750, "Hello, PDF World!")
p.showPage()
p.save()
buffer.seek(0)
response = HttpResponse(buffer.getvalue(), content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="document.pdf"'
return response
def streaming_response(request):
"""Return streaming response for large data"""
def generate_data():
for i in range(10000):
yield f"Line {i}\n"
response = StreamingHttpResponse(
generate_data(),
content_type='text/plain'
)
response['Content-Disposition'] = 'attachment; filename="large_file.txt"'
return response
def file_download(request, file_id):
"""Serve file download"""
document = get_object_or_404(Document, id=file_id)
# Check permissions
if not can_download_file(request.user, document):
return HttpResponseForbidden("Access denied")
response = FileResponse(
document.file.open('rb'),
as_attachment=True,
filename=document.original_filename
)
return response
def template_response_view(request):
"""Return TemplateResponse for lazy rendering"""
context = {'posts': Post.objects.all()}
# TemplateResponse allows middleware to modify context
return TemplateResponse(request, 'blog/post_list.html', context)
from django.contrib import messages
from django.core.paginator import Paginator
from .forms import PostForm, CommentForm, SearchForm
def post_list_with_search(request):
"""Post list with search functionality"""
form = SearchForm(request.GET or None)
posts = Post.objects.filter(status='published')
if form.is_valid():
query = form.cleaned_data.get('query')
category = form.cleaned_data.get('category')
date_from = form.cleaned_data.get('date_from')
date_to = form.cleaned_data.get('date_to')
if query:
posts = posts.filter(
Q(title__icontains=query) | Q(content__icontains=query)
)
if category:
posts = posts.filter(category=category)
if date_from:
posts = posts.filter(created_at__gte=date_from)
if date_to:
posts = posts.filter(created_at__lte=date_to)
# Pagination
paginator = Paginator(posts, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'form': form,
'page_obj': page_obj,
'posts': page_obj.object_list,
}
return render(request, 'blog/post_list.html', context)
@login_required
def post_edit(request, pk):
"""Edit existing post"""
post = get_object_or_404(Post, pk=pk)
# Check permissions
if post.author != request.user and not request.user.is_staff:
messages.error(request, "You don't have permission to edit this post.")
return redirect('blog:detail', pk=post.pk)
if request.method == 'POST':
form = PostForm(request.POST, request.FILES, instance=post)
if form.is_valid():
post = form.save()
messages.success(request, 'Post updated successfully!')
return redirect('blog:detail', pk=post.pk)
else:
messages.error(request, 'Please correct the errors below.')
else:
form = PostForm(instance=post)
context = {
'form': form,
'post': post,
'is_edit': True,
}
return render(request, 'blog/post_form.html', context)
def post_with_comments(request, pk):
"""Post detail with comment form"""
post = get_object_or_404(Post, pk=pk, status='published')
comments = post.comments.filter(approved=True).order_by('created_at')
# Handle comment submission
if request.method == 'POST' and request.user.is_authenticated:
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
messages.success(request, 'Comment added successfully!')
return redirect('blog:detail', pk=post.pk)
else:
comment_form = CommentForm() if request.user.is_authenticated else None
context = {
'post': post,
'comments': comments,
'comment_form': comment_form,
}
return render(request, 'blog/post_detail.html', context)
import os
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from PIL import Image
@login_required
def upload_image(request):
"""Handle image upload with processing"""
if request.method == 'POST':
uploaded_file = request.FILES.get('image')
if not uploaded_file:
messages.error(request, 'No file selected.')
return render(request, 'blog/upload_form.html')
# Validate file type
if not uploaded_file.content_type.startswith('image/'):
messages.error(request, 'Please upload an image file.')
return render(request, 'blog/upload_form.html')
# Validate file size (5MB limit)
if uploaded_file.size > 5 * 1024 * 1024:
messages.error(request, 'File size must be less than 5MB.')
return render(request, 'blog/upload_form.html')
try:
# Process image
image = Image.open(uploaded_file)
# Resize if too large
max_size = (1200, 1200)
if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
image.thumbnail(max_size, Image.Resampling.LANCZOS)
# Save processed image
output = io.BytesIO()
image_format = image.format or 'JPEG'
image.save(output, format=image_format, quality=85, optimize=True)
output.seek(0)
# Generate filename
filename = f"uploads/{request.user.id}/{uploaded_file.name}"
# Save to storage
path = default_storage.save(filename, ContentFile(output.read()))
# Create database record
user_image = UserImage.objects.create(
user=request.user,
image=path,
original_filename=uploaded_file.name
)
messages.success(request, 'Image uploaded successfully!')
return redirect('blog:image_detail', pk=user_image.pk)
except Exception as e:
messages.error(request, f'Error processing image: {str(e)}')
return render(request, 'blog/upload_form.html')
def bulk_upload(request):
"""Handle multiple file uploads"""
if request.method == 'POST':
files = request.FILES.getlist('files')
if not files:
messages.error(request, 'No files selected.')
return render(request, 'blog/bulk_upload.html')
uploaded_count = 0
errors = []
for uploaded_file in files:
try:
# Validate and process each file
if uploaded_file.size > 10 * 1024 * 1024: # 10MB limit
errors.append(f'{uploaded_file.name}: File too large')
continue
# Save file
document = Document.objects.create(
title=uploaded_file.name,
file=uploaded_file,
uploaded_by=request.user
)
uploaded_count += 1
except Exception as e:
errors.append(f'{uploaded_file.name}: {str(e)}')
if uploaded_count:
messages.success(request, f'{uploaded_count} files uploaded successfully!')
if errors:
for error in errors:
messages.error(request, error)
return render(request, 'blog/bulk_upload.html')
from django.shortcuts import render
from django.http import HttpResponseNotFound, HttpResponseServerError
import logging
logger = logging.getLogger(__name__)
def custom_404(request, exception):
"""Custom 404 error page"""
context = {
'request_path': request.path,
'popular_posts': Post.objects.filter(status='published')[:5],
}
return render(request, 'errors/404.html', context, status=404)
def custom_500(request):
"""Custom 500 error page"""
logger.error(f'500 error on {request.path}', extra={
'request': request,
})
return render(request, 'errors/500.html', status=500)
def custom_403(request, exception):
"""Custom 403 error page"""
context = {
'message': str(exception) if exception else 'Access denied',
}
return render(request, 'errors/403.html', context, status=403)
def post_detail_with_error_handling(request, pk):
"""Post detail with comprehensive error handling"""
try:
post = get_object_or_404(Post, pk=pk)
# Check if post is published or user has permission
if post.status != 'published':
if not request.user.is_authenticated:
messages.error(request, 'Please log in to view this post.')
return redirect('accounts:login')
if post.author != request.user and not request.user.is_staff:
return HttpResponseForbidden('You do not have permission to view this post.')
# Increment view count safely
try:
Post.objects.filter(pk=pk).update(views=F('views') + 1)
except Exception as e:
logger.warning(f'Failed to increment view count for post {pk}: {e}')
context = {'post': post}
return render(request, 'blog/post_detail.html', context)
except Http404:
# Log 404 for analytics
logger.info(f'404 for post {pk} from {request.META.get("REMOTE_ADDR")}')
raise
except Exception as e:
# Log unexpected errors
logger.error(f'Unexpected error in post_detail: {e}', extra={
'request': request,
'post_pk': pk,
})
# Return generic error response
return HttpResponseServerError('An error occurred while loading the post.')
from django.core.exceptions import ValidationError
from django.contrib import messages
def post_create_with_validation(request):
"""Post creation with custom validation"""
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
# Custom validation
try:
# Check if user has reached post limit
user_post_count = Post.objects.filter(author=request.user).count()
if user_post_count >= 10 and not request.user.is_premium:
raise ValidationError('Free users can only create 10 posts. Upgrade to premium for unlimited posts.')
# Check for duplicate titles
title = request.POST.get('title', '').strip()
if title and Post.objects.filter(title__iexact=title, author=request.user).exists():
raise ValidationError('You already have a post with this title.')
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
# Additional processing
if 'save_draft' in request.POST:
post.status = 'draft'
messages.info(request, 'Post saved as draft.')
else:
post.status = 'published'
messages.success(request, 'Post published successfully!')
post.save()
form.save_m2m()
return redirect('blog:detail', pk=post.pk)
except ValidationError as e:
messages.error(request, str(e))
except Exception as e:
logger.error(f'Error creating post: {e}', extra={'request': request})
messages.error(request, 'An error occurred while creating the post.')
else:
form = PostForm()
context = {
'form': form,
'user_post_count': Post.objects.filter(author=request.user).count(),
'is_premium': request.user.is_premium if hasattr(request.user, 'is_premium') else False,
}
return render(request, 'blog/post_form.html', context)
from functools import wraps
from django.http import JsonResponse
from django.core.cache import cache
import time
def ajax_required(view_func):
"""Decorator to require AJAX requests"""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'error': 'AJAX request required'}, status=400)
return view_func(request, *args, **kwargs)
return wrapper
def rate_limit(requests_per_minute=60):
"""Rate limiting decorator"""
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
# Create cache key based on IP and view
cache_key = f'rate_limit:{request.META.get("REMOTE_ADDR")}:{view_func.__name__}'
# Get current request count
current_requests = cache.get(cache_key, 0)
if current_requests >= requests_per_minute:
return JsonResponse({
'error': 'Rate limit exceeded. Please try again later.'
}, status=429)
# Increment counter
cache.set(cache_key, current_requests + 1, 60) # 1 minute timeout
return view_func(request, *args, **kwargs)
return wrapper
return decorator
def measure_time(view_func):
"""Decorator to measure 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
# Add execution time to response headers
response['X-Execution-Time'] = f'{execution_time:.3f}s'
# Log slow requests
if execution_time > 1.0: # Log requests taking more than 1 second
logger.warning(f'Slow request: {request.path} took {execution_time:.3f}s')
return response
return wrapper
# Usage examples
@ajax_required
@rate_limit(requests_per_minute=30)
def api_post_create(request):
"""AJAX endpoint for creating posts"""
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return JsonResponse({
'success': True,
'post_id': post.pk,
'post_url': reverse('blog:detail', kwargs={'pk': post.pk})
})
else:
return JsonResponse({
'success': False,
'errors': form.errors
})
return JsonResponse({'error': 'POST method required'}, status=405)
@measure_time
def expensive_view(request):
"""View with performance monitoring"""
# Simulate expensive operation
posts = Post.objects.select_related('author', 'category').prefetch_related('tags')
# Complex processing
processed_posts = []
for post in posts:
processed_posts.append({
'post': post,
'word_count': len(post.content.split()),
'reading_time': len(post.content.split()) // 200, # Assume 200 WPM
})
return render(request, 'blog/expensive_view.html', {
'processed_posts': processed_posts
})
Function-based views provide direct, explicit control over request handling. They're perfect for custom logic, simple operations, and situations where you need fine-grained control over the request-response cycle. Understanding FBVs is essential for Django development and provides the foundation for more advanced view patterns.
The URL Dispatcher
Django's URL dispatcher is a powerful pattern-matching system that maps incoming URLs to view functions. It provides clean, readable URLs while maintaining flexibility for complex routing requirements.
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.