URLs and Views

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.

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.

View Function Basics

Simple View Structure

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

Request Object

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)

Response Types

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)

Form Handling

GET and POST Processing

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)

File Upload Handling

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

Error Handling

Custom Error Views

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

Validation and Error Messages

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)

View Utilities and Helpers

Custom View Decorators

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.