Views and URL Patterns

Module 23: Web Frameworks II (Python) - Wednesday, Lecture 3

Introduction to Views and URLs

Views and URL patterns work together to form the "controller" part of Django's MVT (Model-View-Template) architecture. They handle the flow of HTTP requests through your application:

graph LR A[Browser] -->|HTTP Request| B[Django Server] B -->|Match URL| C[URLConf] C -->|Call View| D[View Function/Class] D -->|Process Request| D D -->|Query| E[(Database)] E -->|Return Data| D D -->|Render| F[Template] F -->|HTML| G[HTTP Response] G --> A

The Postal Service Analogy

Think of Django's URL and view system like a postal service:

  • The URL patterns are like the sorting facility that routes mail based on the address
  • The views are like postal workers who process mail at the destination
  • The URL parameters are like apartment numbers that specify exactly where the mail should go
  • The HTTP methods (GET, POST, etc.) are like different types of mail (letters, packages, express mail)

Just as a postal service has a systematic way of routing mail to the right destinations, Django has a structured approach to routing and processing web requests.

URL Configuration

Django's URL configuration maps URL patterns to view functions or classes. URLs are defined in urls.py files, typically at both project and app levels.

Project-Level URLs

The project-level urls.py routes requests to app-specific URL configurations:

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('accounts/', include('accounts.urls')),
    path('', include('pages.urls')),
]

# Serve media files in development
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

App-Level URLs

Each app typically has its own urls.py for app-specific URL patterns:

# blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'  # Application namespace for URL naming

urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('////', 
         views.post_detail, 
         name='post_detail'),
    path('category//', 
         views.post_list_by_category, 
         name='post_list_by_category'),
    path('tag//', 
         views.post_list_by_tag, 
         name='post_list_by_tag'),
    path('author//', 
         views.post_list_by_author, 
         name='post_list_by_author'),
    path('search/', views.post_search, name='post_search'),
]

The app_name provides a namespace for URL names, allowing you to have the same URL name in different apps without conflicts. You can then reference these URLs in templates and views using the namespace:

# In a template
<a href="{% url 'blog:post_detail' year=post.published_at.year 
                              month=post.published_at.month 
                              day=post.published_at.day 
                              slug=post.slug %}">
    {{ post.title }}
</a>

# In a view
from django.urls import reverse

def some_view(request):
    url = reverse('blog:post_detail', args=[2023, 9, 15, 'my-first-post'])
    # or
    url = reverse('blog:post_detail', kwargs={
        'year': 2023,
        'month': 9,
        'day': 15,
        'slug': 'my-first-post'
    })

URL Pattern Syntax

Django provides several ways to define URL patterns:

Path Converters

Path converters capture path segments and convert them to the specified type:

from django.urls import path

urlpatterns = [
    # Basic path with no parameters
    path('about/', views.about, name='about'),
    
    # Path with string parameter
    path('product//', views.product_detail, name='product_detail'),
    
    # Path with integer parameter
    path('products//', views.product_list, name='product_list'),
    
    # Path with multiple parameters
    path('archive///', views.archive, name='archive'),
    
    # Path with slug parameter (letters, numbers, underscores, hyphens)
    path('article//', views.article_detail, name='article_detail'),
    
    # Path with UUID parameter
    path('order//', views.order_detail, name='order_detail'),
    
    # Path with path parameter (includes slashes)
    path('files//', views.serve_file, name='serve_file'),
]
Converter Description Example
str Matches any non-empty string, excluding the path separator (/) <str:username>
int Matches zero or any positive integer <int:year>
slug Matches any slug string (ASCII letters, numbers, hyphens, underscores) <slug:post_slug>
uuid Matches a UUID formatted string <uuid:id>
path Matches any non-empty string, including the path separator <path:file_path>

Regular Expressions (re_path)

For more complex URL patterns, you can use regular expressions with re_path:

from django.urls import path, re_path

urlpatterns = [
    # Match a 4-digit year followed by a month (01-12)
    re_path(r'^archive/(?P[0-9]{4})/(?P0[1-9]|1[0-2])/$', 
            views.archive, 
            name='archive'),
    
    # Match a username (letters, numbers, underscores)
    re_path(r'^user/(?P\w+)/$', 
            views.user_profile, 
            name='user_profile'),
            
    # Match a product code (format: ABC-12345)
    re_path(r'^product/(?P[A-Z]{3}-[0-9]{5})/$', 
            views.product_detail, 
            name='product_detail'),
]

With re_path, you can create more flexible and precise URL patterns, but at the cost of readability.

Function-Based Views

Function-based views are Python functions that take a request object and return a response object. They are simple and straightforward for basic use cases.

Basic View Example

from django.shortcuts import render, get_object_or_404
from .models import Post

def post_list(request):
    """View to display a list of all published posts."""
    posts = Post.objects.filter(status='published')
    return render(request, 'blog/post_list.html', {'posts': posts})

def post_detail(request, year, month, day, slug):
    """View to display a single post."""
    post = get_object_or_404(Post, 
                            slug=slug,
                            status='published',
                            published_at__year=year,
                            published_at__month=month,
                            published_at__day=day)
    
    # Increment the view count
    post.views += 1
    post.save()
    
    # Get comments for this post
    comments = post.comments.filter(approved=True)
    
    return render(request, 'blog/post_detail.html', {
        'post': post,
        'comments': comments,
    })

Function-based views typically follow this pattern:

  1. Receive the request and any URL parameters
  2. Process the request (fetch data, handle forms, etc.)
  3. Return a response (rendered template, redirect, etc.)

Request and Response Objects

The request object contains information about the current request:

# Request properties
request.method  # 'GET', 'POST', etc.
request.GET     # Query parameters
request.POST    # Form data for POST requests
request.FILES   # Uploaded files
request.COOKIES # Browser cookies
request.session # Session data
request.user    # Current user (AnonymousUser or User instance)
request.path    # URL path
request.META    # HTTP headers and server info

Common response types:

from django.shortcuts import render, redirect
from django.http import HttpResponse, JsonResponse

# HTML response from a template
def template_view(request):
    context = {'key': 'value'}
    return render(request, 'template.html', context)

# Simple HTTP response
def simple_view(request):
    return HttpResponse("Hello, world!")

# Redirect response
def redirect_view(request):
    return redirect('view_name')  # Redirect to a URL name
    # or
    return redirect('/some/url/')  # Redirect to a URL path
    # or
    return redirect('https://example.com/')  # Redirect to an absolute URL

# JSON response
def api_view(request):
    data = {'key': 'value', 'list': [1, 2, 3]}
    return JsonResponse(data)

# Custom status code
def not_found_view(request):
    return HttpResponse("Not Found", status=404)

Form Handling in Views

Function-based views often handle form submissions. Here's a typical pattern for processing forms:

from django.shortcuts import render, redirect
from .forms import CommentForm
from .models import Post

def add_comment(request, post_id):
    """View to add a comment to a post."""
    post = get_object_or_404(Post, id=post_id)
    
    if request.method == 'POST':
        # Create a form instance with the submitted data
        form = CommentForm(request.POST)
        
        if form.is_valid():
            # Create a comment object but don't save it yet
            comment = form.save(commit=False)
            
            # Add the post to the comment
            comment.post = post
            
            # Add the user if authenticated
            if request.user.is_authenticated:
                comment.author = request.user
            
            # Save the comment
            comment.save()
            
            # Redirect to the post detail page
            return redirect('blog:post_detail', 
                          year=post.published_at.year,
                          month=post.published_at.month,
                          day=post.published_at.day,
                          slug=post.slug)
    else:
        # Create an empty form
        form = CommentForm()
    
    return render(request, 'blog/add_comment.html', {
        'form': form,
        'post': post,
    })

This pattern follows these steps:

  1. Check if the request method is POST (form submission)
  2. If POST, create a form instance with the submitted data
  3. Validate the form data
  4. If valid, process the form data (e.g., save to the database)
  5. Redirect to avoid duplicate form submissions
  6. If not POST or form invalid, render the template with the form

The Post/Redirect/Get Pattern

Always redirect after successful form submission to prevent duplicate submissions if the user refreshes the page. This is known as the Post/Redirect/Get (PRG) pattern:

  1. User posts a form
  2. Server processes the form and redirects to a success page
  3. User's browser gets the success page

This pattern prevents the "form resubmission" warning in browsers and helps avoid duplicate data.

Class-Based Views

Class-based views (CBVs) provide a way to organize view code by using classes instead of functions. They promote code reuse and follow object-oriented principles.

Basic Class-Based View

from django.views.generic import View
from django.shortcuts import render, get_object_or_404
from .models import Post

class PostListView(View):
    """Class-based view to display a list of posts."""
    
    def get(self, request):
        """Handle GET requests."""
        posts = Post.objects.filter(status='published')
        return render(request, 'blog/post_list.html', {'posts': posts})

The URL pattern for a class-based view uses as_view():

# In urls.py
path('', views.PostListView.as_view(), name='post_list'),

Generic Class-Based Views

Django provides many generic class-based views for common patterns:

ListView

from django.views.generic import ListView
from .models import Post

class PostListView(ListView):
    model = Post  # The model to query
    template_name = 'blog/post_list.html'  # Template (default: post_list.html)
    context_object_name = 'posts'  # Name in template (default: post_list)
    paginate_by = 10  # Pagination (10 posts per page)
    
    def get_queryset(self):
        """Override to customize the queryset."""
        return Post.objects.filter(status='published')

DetailView

from django.views.generic import DetailView
from .models import Post

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    
    def get_queryset(self):
        """Customize the queryset."""
        return Post.objects.filter(status='published')
    
    def get_context_data(self, **kwargs):
        """Add additional context."""
        context = super().get_context_data(**kwargs)
        context['comments'] = self.object.comments.filter(approved=True)
        return context

FormView

from django.views.generic.edit import FormView
from django.urls import reverse_lazy
from .forms import ContactForm

class ContactView(FormView):
    template_name = 'contact.html'
    form_class = ContactForm
    success_url = reverse_lazy('contact_success')
    
    def form_valid(self, form):
        """Process valid form data."""
        # Process the form
        name = form.cleaned_data['name']
        email = form.cleaned_data['email']
        message = form.cleaned_data['message']
        
        # Send email, save to database, etc.
        send_contact_email(name, email, message)
        
        return super().form_valid(form)

CreateView, UpdateView, DeleteView

from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm

class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    
    def form_valid(self, form):
        """Set the author to the current user."""
        form.instance.author = self.request.user
        return super().form_valid(form)

class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    
    def get_queryset(self):
        """Only allow editing of user's own posts."""
        return Post.objects.filter(author=self.request.user)

class PostDeleteView(DeleteView):
    model = Post
    template_name = 'blog/post_confirm_delete.html'
    success_url = reverse_lazy('blog:post_list')
    
    def get_queryset(self):
        """Only allow deleting of user's own posts."""
        return Post.objects.filter(author=self.request.user)
classDiagram class View { +dispatch() +http_method_not_allowed() +as_view() } class TemplateView { +template_name +get_context_data() +get() } class ListView { +model +queryset +paginate_by +context_object_name +get_queryset() +get_context_data() } class DetailView { +model +queryset +context_object_name +get_object() +get_context_data() } class FormView { +form_class +success_url +get_form() +form_valid() +form_invalid() } class CreateView { +model +fields +form_class +success_url +get_form() +form_valid() } View <|-- TemplateView View <|-- ListView View <|-- DetailView View <|-- FormView FormView <|-- CreateView

Advanced Class-Based Views

Class-based views can be customized and combined in powerful ways:

Mixins

Mixins are classes that provide specific functionality that can be "mixed in" to views:

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import DetailView
from .models import Post

class PostDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    login_url = '/login/'  # Redirect URL for non-authenticated users
    
    def test_func(self):
        """Check if the user can access this post."""
        post = self.get_object()
        return post.status == 'published' or self.request.user == post.author

Common built-in mixins:

Custom Mixins

You can create your own mixins to reuse functionality across views:

class AuthorRequiredMixin:
    """Mixin to require the current user to be the author."""
    
    def dispatch(self, request, *args, **kwargs):
        """Check if the user is the author."""
        # Get the object first
        obj = self.get_object()
        
        if request.user != obj.author:
            # User is not the author
            return self.handle_no_permission()
        
        return super().dispatch(request, *args, **kwargs)
    
    def handle_no_permission(self):
        """Handle unauthorized access."""
        messages.error(self.request, "You are not the author of this post.")
        return redirect('blog:post_list')

class PostUpdateView(LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'

Method Overrides

Class-based views have many methods you can override to customize behavior:

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    
    def get_object(self, queryset=None):
        """Get the post using slug, year, month, day."""
        queryset = self.get_queryset()
        
        year = self.kwargs.get('year')
        month = self.kwargs.get('month')
        day = self.kwargs.get('day')
        slug = self.kwargs.get('slug')
        
        return get_object_or_404(queryset,
                                published_at__year=year,
                                published_at__month=month,
                                published_at__day=day,
                                slug=slug,
                                status='published')
    
    def get_context_data(self, **kwargs):
        """Add extra context data."""
        context = super().get_context_data(**kwargs)
        
        # Add comments to context
        context['comments'] = self.object.comments.filter(approved=True)
        
        # Add comment form to context
        context['comment_form'] = CommentForm()
        
        # Add related posts
        context['related_posts'] = self.object.get_related_posts()
        
        return context
    
    def dispatch(self, request, *args, **kwargs):
        """Called before any HTTP method handlers."""
        # Increment the view count
        post = self.get_object()
        post.views += 1
        post.save()
        
        return super().dispatch(request, *args, **kwargs)

Multiple Forms in a View

For more complex views, you might need to handle multiple forms:

from django.views.generic import View
from .forms import PostForm, ImageFormSet

class PostCreateWithImagesView(LoginRequiredMixin, View):
    template_name = 'blog/post_with_images_form.html'
    
    def get(self, request):
        """Handle GET requests."""
        post_form = PostForm()
        image_formset = ImageFormSet(prefix='images')
        
        return render(request, self.template_name, {
            'post_form': post_form,
            'image_formset': image_formset,
        })
    
    def post(self, request):
        """Handle POST requests."""
        post_form = PostForm(request.POST)
        image_formset = ImageFormSet(request.POST, request.FILES, prefix='images')
        
        if post_form.is_valid() and image_formset.is_valid():
            # Save the post
            post = post_form.save(commit=False)
            post.author = request.user
            post.save()
            
            # Save the images
            images = image_formset.save(commit=False)
            for image in images:
                image.post = post
                image.save()
            
            return redirect('blog:post_detail', 
                          year=post.published_at.year,
                          month=post.published_at.month,
                          day=post.published_at.day,
                          slug=post.slug)
        
        return render(request, self.template_name, {
            'post_form': post_form,
            'image_formset': image_formset,
        })

Function-Based vs. Class-Based Views

Both function-based views (FBVs) and class-based views (CBVs) have their strengths and appropriate use cases:

Aspect Function-Based Views Class-Based Views
Simplicity Simple and straightforward for basic views More complex with a steeper learning curve
Readability Easy to follow the code flow Can be harder to trace code execution
Code Reuse Less reusable, may lead to duplication Better code reuse through inheritance and mixins
HTTP Method Handling Manual handling with if statements Methods map directly to HTTP verbs (get, post, etc.)
Extension Harder to extend existing views Easy to extend using inheritance and mixins
Common Patterns Must implement common patterns manually Many common patterns built-in (list, detail, form, etc.)
Customization Complete freedom in implementation Sometimes constrained by the class structure
Learning Curve Low, easy to get started Higher, requires understanding class inheritance

When to Use Each Approach

Use function-based views when:

  • The view is simple and doesn't require much code
  • The view doesn't fit well into Django's generic patterns
  • You need more control over the request/response flow
  • You're more comfortable with procedural programming

Use class-based views when:

  • The view fits a common pattern (list, detail, form, etc.)
  • You need to reuse code across multiple views
  • You're implementing standard CRUD operations
  • You're comfortable with object-oriented programming

URL Routing Advanced Topics

Django's URL routing system has several advanced features:

Reverse URL Resolution

Reverse URL resolution helps avoid hardcoding URLs in your code and templates:

# In views
from django.urls import reverse, reverse_lazy

# For redirects
def my_view(request):
    return redirect(reverse('blog:post_detail', kwargs={
        'year': 2023,
        'month': 9,
        'day': 15,
        'slug': 'my-post'
    }))

# For class-based views (success_url)
class PostDeleteView(DeleteView):
    model = Post
    success_url = reverse_lazy('blog:post_list')  # Use reverse_lazy for attributes

# In templates
<a href="{% url 'blog:post_detail' year=2023 month=9 day=15 slug='my-post' %}">
    My Post
</a>

The reverse() function resolves a URL pattern name to a URL. reverse_lazy() is a lazily evaluated version that's safe to use in class attributes.

Including Other URLconfs

You can include other URL configurations with additional options:

# Include with namespace
path('blog/', include(('blog.urls', 'blog'), namespace='blog')),

# Include with a prefix
path('api/v1/', include('myapp.urls')),

# Include with app_name already set in the included urls.py
path('blog/', include('blog.urls')),  # blog/urls.py has app_name = 'blog'

URL Patterns as Python Lists

You can create URL patterns programmatically:

# Dynamic URL patterns
urlpatterns = [
    path('admin/', admin.site.urls),
]

# Add patterns for each installed app
for app_config in apps.get_app_configs():
    if hasattr(app_config, 'urls_module'):
        urlpatterns.append(
            path(f'{app_config.label}/', include(f'{app_config.name}.urls'))
        )

Default Parameters

You can provide default parameters for view functions:

# urls.py
path('blog/', views.post_list, {'order': '-published_at'}, name='post_list'),

# views.py
def post_list(request, order='-created_at'):
    posts = Post.objects.filter(status='published').order_by(order)
    return render(request, 'blog/post_list.html', {'posts': posts})

Custom Path Converters

You can create custom path converters for special URL patterns:

# converters.py
class YearMonthConverter:
    regex = r'(20\d{2})-(0[1-9]|1[0-2])'
    
    def to_python(self, value):
        year, month = value.split('-')
        return {'year': int(year), 'month': int(month)}
    
    def to_url(self, value):
        return f"{value['year']}-{value['month']:02d}"

# urls.py
from django.urls import path, register_converter
from . import converters, views

register_converter(converters.YearMonthConverter, 'yearmonth')

urlpatterns = [
    path('archive//', views.archive, name='archive'),
]

# views.py
def archive(request, date):
    year = date['year']
    month = date['month']
    # ... get posts for this year and month
    return render(request, 'blog/archive.html', {
        'year': year,
        'month': month,
        'posts': posts,
    })

Middleware and Request/Response Processing

Middleware is a framework of hooks into Django's request/response processing. It's a way to modify the input or output of a Django application globally.

graph TD A[Browser] -->|HTTP Request| B[Django Server] B -->|Process Request| C[Middleware 1] C -->|Process Request| D[Middleware 2] D -->|Process Request| E[Middleware 3] E -->|Process Request| F[View] F -->|Response| G[Middleware 3] G -->|Process Response| H[Middleware 2] H -->|Process Response| I[Middleware 1] I -->|HTTP Response| A

Middleware can perform actions like:

Custom Middleware Example

class RequestLoggingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        
    def __call__(self, request):
        # Code to be executed for each request before the view
        start_time = time.time()
        
        # Call the next middleware or view
        response = self.get_response(request)
        
        # Code to be executed for each response after the view
        duration = time.time() - start_time
        
        # Log the request and timing
        logger.info(
            f"{request.method} {request.path} - {response.status_code} "
            f"({duration:.2f}s)"
        )
        
        return response

To activate the middleware, add it to MIDDLEWARE in settings:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'myapp.middleware.RequestLoggingMiddleware',  # Our custom middleware
]

The order in MIDDLEWARE matters: middleware are processed from top to bottom for requests and from bottom to top for responses.

Real-World Example: Complete Blog Application

Let's examine a comprehensive example of views and URLs for a blog application:

URL Configuration

# blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    # Blog list views
    path('', views.PostListView.as_view(), name='post_list'),
    path('category//', 
         views.PostListView.as_view(), 
         name='post_list_by_category'),
    path('tag//', 
         views.PostListView.as_view(), 
         name='post_list_by_tag'),
    path('author//', 
         views.PostListView.as_view(), 
         name='post_list_by_author'),
    
    # Blog post detail
    path('////', 
         views.PostDetailView.as_view(), 
         name='post_detail'),
    
    # Post CRUD operations
    path('create/', 
         views.PostCreateView.as_view(), 
         name='post_create'),
    path('/edit/', 
         views.PostUpdateView.as_view(), 
         name='post_edit'),
    path('/delete/', 
         views.PostDeleteView.as_view(), 
         name='post_delete'),
    
    # Comments
    path('/comment/', 
         views.CommentCreateView.as_view(), 
         name='add_comment'),
    
    # Search
    path('search/', views.PostSearchView.as_view(), name='post_search'),
]

View Implementations

from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy, reverse
from django.contrib.auth.models import User
from django.db.models import Q

from .models import Post, Category, Tag, Comment
from .forms import PostForm, CommentForm

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 10
    
    def get_queryset(self):
        """Filter posts based on URL parameters."""
        queryset = Post.objects.filter(status='published')
        
        # Filter by category
        if 'category_slug' in self.kwargs:
            category = get_object_or_404(Category, slug=self.kwargs['category_slug'])
            queryset = queryset.filter(category=category)
        
        # Filter by tag
        if 'tag_slug' in self.kwargs:
            tag = get_object_or_404(Tag, slug=self.kwargs['tag_slug'])
            queryset = queryset.filter(tags__in=[tag])
        
        # Filter by author
        if 'username' in self.kwargs:
            author = get_object_or_404(User, username=self.kwargs['username'])
            queryset = queryset.filter(author=author)
        
        return queryset
    
    def get_context_data(self, **kwargs):
        """Add extra context data."""
        context = super().get_context_data(**kwargs)
        
        # Add categories to context
        context['categories'] = Category.objects.all()
        
        # Add filter info to context
        if 'category_slug' in self.kwargs:
            context['category'] = get_object_or_404(
                Category, slug=self.kwargs['category_slug']
            )
        
        if 'tag_slug' in self.kwargs:
            context['tag'] = get_object_or_404(
                Tag, slug=self.kwargs['tag_slug']
            )
        
        if 'username' in self.kwargs:
            context['author'] = get_object_or_404(
                User, username=self.kwargs['username']
            )
        
        return context

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    
    def get_object(self, queryset=None):
        """Get the post using slug, year, month, day."""
        queryset = self.get_queryset()
        
        year = self.kwargs.get('year')
        month = self.kwargs.get('month')
        day = self.kwargs.get('day')
        slug = self.kwargs.get('slug')
        
        return get_object_or_404(queryset,
                                published_at__year=year,
                                published_at__month=month,
                                published_at__day=day,
                                slug=slug,
                                status='published')
    
    def get_context_data(self, **kwargs):
        """Add extra context data."""
        context = super().get_context_data(**kwargs)
        
        # Add comments to context
        context['comments'] = self.object.comments.filter(approved=True)
        
        # Add comment form to context
        context['comment_form'] = CommentForm()
        
        # Increment view count
        self.object.views += 1
        self.object.save(update_fields=['views'])
        
        return context

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    
    def form_valid(self, form):
        """Set the author to the current user."""
        form.instance.author = self.request.user
        return super().form_valid(form)
    
    def get_success_url(self):
        """Return the URL to redirect to after successful form submission."""
        return reverse('blog:post_detail', kwargs={
            'year': self.object.published_at.year,
            'month': self.object.published_at.month,
            'day': self.object.published_at.day,
            'slug': self.object.slug
        })

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    
    def test_func(self):
        """Check if the user is the author."""
        post = self.get_object()
        return self.request.user == post.author
    
    def get_success_url(self):
        """Return the URL to redirect to after successful form submission."""
        return reverse('blog:post_detail', kwargs={
            'year': self.object.published_at.year,
            'month': self.object.published_at.month,
            'day': self.object.published_at.day,
            'slug': self.object.slug
        })

class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Post
    template_name = 'blog/post_confirm_delete.html'
    success_url = reverse_lazy('blog:post_list')
    
    def test_func(self):
        """Check if the user is the author."""
        post = self.get_object()
        return self.request.user == post.author

class CommentCreateView(LoginRequiredMixin, CreateView):
    model = Comment
    form_class = CommentForm
    http_method_names = ['post']  # Only allow POST requests
    
    def form_valid(self, form):
        """Set the post and author."""
        post_id = self.kwargs.get('post_id')
        post = get_object_or_404(Post, id=post_id, status='published')
        
        form.instance.post = post
        form.instance.author = self.request.user
        
        return super().form_valid(form)
    
    def get_success_url(self):
        """Return the URL to redirect to after successful form submission."""
        post = self.object.post
        return reverse('blog:post_detail', kwargs={
            'year': post.published_at.year,
            'month': post.published_at.month,
            'day': post.published_at.day,
            'slug': post.slug
        })

class PostSearchView(ListView):
    model = Post
    template_name = 'blog/search_results.html'
    context_object_name = 'posts'
    paginate_by = 10
    
    def get_queryset(self):
        """Filter posts based on search query."""
        query = self.request.GET.get('q', '')
        
        if query:
            return Post.objects.filter(
                Q(title__icontains=query) | 
                Q(content__icontains=query),
                status='published'
            )
        
        return Post.objects.none()
    
    def get_context_data(self, **kwargs):
        """Add search query to context."""
        context = super().get_context_data(**kwargs)
        context['query'] = self.request.GET.get('q', '')
        return context

This example demonstrates a comprehensive blog application with:

Best Practices for Views and URLs

Here are some best practices to follow when designing views and URL patterns:

URL Design

View Design

Performance Considerations

Security Considerations

Practice Activity

Implement views and URL patterns for a simple task management application with the following features:

  1. User registration and authentication
  2. Task list view (all tasks, completed tasks, pending tasks)
  3. Task detail view
  4. Task creation, editing, and deletion
  5. Task categories (filtering by category)
  6. Task search

Implement the following:

Bonus challenges:

Further Topics to Explore