Module 7: Views.py — Request Handling & URL Routing
🎯 What You Will Learn in This Module
By the end of this tutorial, you will:
- Understand the request/response cycle in Django
- Build Function-Based Views (FBV) for full control over logic
- Build Class-Based Views (CBV) for rapid, reusable development
- Master Django's generic views:
ListView,DetailView,CreateView,UpdateView,DeleteView - Implement pagination and search functionality
- Protect views with
@login_requiredandLoginRequiredMixin - Enforce object-level permissions (only authors can edit their posts)
- Configure URL routing with named URLs and the
app_namenamespace
📖 1. What is a View?
A view is a Python function or class that receives an HTTP request, processes it, and returns an HTTP response. It is the brain of your web page — it decides what data to fetch, how to process it, and what to show the user.
Browser → URL Dispatcher → View → Model (fetch data) → Template (render HTML) → Browser
Django gives you two ways to write views:
| Approach | Best For | Learning Curve |
|---|---|---|
| Function-Based Views (FBV) | Custom logic, APIs, simple pages | Easier to understand |
| Class-Based Views (CBV) | CRUD operations, reusable patterns | Steeper, but less code |
💡 Rule of thumb: Start with FBVs to understand the flow. Switch to CBVs when you find yourself repeating the same patterns.
📝 2. Function-Based Views (FBV)
FBVs are plain Python functions that take a request object and return a response. They give you complete control.
The Homepage View
1# blog/views.py 2 3from django.shortcuts import render, get_object_or_404, redirect 4from django.core.paginator import Paginator 5from django.db.models import Q, Count, F 6from django.contrib.auth.decorators import login_required 7from .models import Post, Category 8 9 10def home(request): 11 """ 12 Homepage view: displays featured posts, latest posts, and active categories. 13 """ 14 # Fetch featured posts (top 5) 15 featured_posts = Post.objects.filter( 16 is_featured=True, 17 status='published' 18 ).select_related('author', 'category')[:5] 19 20 # Fetch latest published posts (top 10) 21 latest_posts = Post.objects.filter( 22 status='published' 23 ).select_related('author', 'category').prefetch_related('tags')[:10] 24 25 # Fetch active categories with post count annotation 26 categories = Category.objects.annotate( 27 post_count=Count('posts') 28 ).filter(is_active=True) 29 30 # Pass data to the template via context dictionary 31 context = { 32 'featured_posts': featured_posts, 33 'latest_posts': latest_posts, 34 'categories': categories, 35 } 36 37 return render(request, 'blog/home.html', context)
What's happening here?
Post.objects.filter(...)queries the database for matching recordsselect_related('author', 'category')uses SQL JOINs to fetch related data in one queryprefetch_related('tags')fetches tags in a separate query (efficient for ManyToMany)annotate(post_count=Count('posts'))adds a calculated field to each categoryrender(request, template, context)returns an HTTP response with the rendered HTML
The Post Detail View
1def post_detail(request, slug): 2 """ 3 Single post view: displays full post content, increments view count, 4 and shows related posts from the same category. 5 """ 6 # Fetch the post or return 404 if not found/published 7 post = get_object_or_404( 8 Post.objects.select_related('author', 'category') 9 .prefetch_related('tags', 'comments__author'), 10 slug=slug, 11 status='published' 12 ) 13 14 # Increment view count efficiently using F() expression 15 # F() avoids race conditions by telling the database to increment directly 16 Post.objects.filter(pk=post.pk).update(view_count=F('view_count') + 1) 17 18 # Fetch 3 related posts from the same category (exclude current post) 19 related_posts = Post.objects.filter( 20 category=post.category, 21 status='published' 22 ).exclude(pk=post.pk)[:3] 23 24 context = { 25 'post': post, 26 'related_posts': related_posts, 27 'reading_time': post.reading_time, 28 } 29 30 return render(request, 'blog/post_detail.html', context)
Key concepts:
get_object_or_404(Model, **kwargs): Fetches one object or raises Http404 automaticallyF('view_count'): Database-level increment. Safer thanpost.view_count += 1; post.save()in high-traffic sites.exclude(pk=post.pk): Removes the current post from related results
The Search View with Pagination
1def search_posts(request): 2 """ 3 Search view: filters posts by title, content, or tag name. 4 Results are paginated (10 per page). 5 """ 6 # Get the search query from URL parameters (?q=search-term) 7 query = request.GET.get('q', '') 8 9 # Filter published posts matching the query 10 posts = Post.objects.filter( 11 status='published' 12 ).filter( 13 Q(title__icontains=query) | 14 Q(content__icontains=query) | 15 Q(tags__name__icontains=query) 16 ).distinct() # distinct() prevents duplicates when matching multiple tags 17 18 # Paginate results: 10 posts per page 19 paginator = Paginator(posts, 10) 20 page_number = request.GET.get('page') 21 page_obj = paginator.get_page(page_number) 22 23 return render(request, 'blog/search.html', { 24 'page_obj': page_obj, 25 'query': query, 26 'total_results': posts.count(), 27 })
Key concepts:
request.GET: Dictionary-like object containing URL query parametersQ(): Allows complex OR queries (match title OR content OR tag)__icontains: Case-insensitive "contains" lookupPaginator: Splits a large queryset into pagesget_page(page_number): Returns the requested page, or page 1 if invalid
The Protected Create View
1@login_required 2def create_post(request): 3 """ 4 Post creation view. Only accessible to logged-in users. 5 """ 6 if request.method == 'POST': 7 # Handle form submission (covered in Module 8) 8 pass 9 10 return render(request, 'blog/create_post.html')
Key concept:
@login_required: Decorator that redirects anonymous users to the login page
🏛️ 3. Class-Based Views (CBV)
CBVs use Python inheritance to reuse common patterns. Django provides generic views that handle 90% of common web development tasks.
Post List View
1from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView 2from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin 3from django.urls import reverse_lazy 4 5 6class PostListView(ListView): 7 """ 8 Displays a paginated list of published posts. 9 Equivalent to the FBV pattern but with less code. 10 """ 11 model = Post 12 template_name = 'blog/post_list.html' # Template to render 13 context_object_name = 'posts' # Name used in template 14 paginate_by = 10 # Items per page 15 16 def get_queryset(self): 17 """ 18 Override to filter only published posts and optimize queries. 19 """ 20 return Post.objects.filter( 21 status='published' 22 ).select_related('author') 23 24 def get_context_data(self, **kwargs): 25 """ 26 Add extra context (categories) to the template. 27 """ 28 context = super().get_context_data(**kwargs) 29 context['categories'] = Category.objects.all() 30 return context
What ListView does automatically:
- Fetches all objects:
Post.objects.all() - Paginates them according to
paginate_by - Passes them to the template as
context_object_name - Handles
?page=2URL parameters automatically
Post Detail View
1class PostDetailView(DetailView): 2 """ 3 Displays a single published post. 4 """ 5 model = Post 6 template_name = 'blog/post_detail.html' 7 slug_url_kwarg = 'slug' # URL parameter name 8 query_pk_and_slug = True # More efficient lookup using both fields 9 10 def get_queryset(self): 11 """ 12 Ensure only published posts are accessible. 13 """ 14 return Post.objects.filter(status='published')
What DetailView does automatically:
- Fetches the object by
slug(orpk) - Passes it to the template as
object(orpost, based on model name) - Returns 404 if not found
Post Create View
1class PostCreateView(LoginRequiredMixin, CreateView): 2 """ 3 Allows authenticated users to create new posts. 4 """ 5 model = Post 6 fields = ['title', 'content', 'category', 'tags', 'featured_image'] 7 template_name = 'blog/post_form.html' 8 9 def form_valid(self, form): 10 """ 11 Automatically set the author and generate slug before saving. 12 """ 13 form.instance.author = self.request.user 14 form.instance.slug = slugify(form.instance.title) 15 return super().form_valid(form) 16 17 def get_success_url(self): 18 """ 19 Redirect to the newly created post's detail page. 20 """ 21 return reverse_lazy('blog:post-detail', kwargs={'slug': self.object.slug})
What CreateView does automatically:
- Generates and displays a form based on
fields - Validates submitted data
- Saves the object to the database
- Redirects to
success_urlafter saving
Post Update View (with Ownership Check)
1class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): 2 """ 3 Allows authors or staff to edit existing posts. 4 """ 5 model = Post 6 fields = ['title', 'content', 'category', 'tags', 'status'] 7 template_name = 'blog/post_form.html' 8 9 def test_func(self): 10 """ 11 Object-level permission: only the author or staff can edit. 12 """ 13 post = self.get_object() 14 return self.request.user == post.author or self.request.user.is_staff
What UpdateView does automatically:
- Fetches the existing object
- Pre-populates the form with current data
- Saves changes when submitted
What UserPassesTestMixin does:
- Runs
test_func()before allowing access - Returns 403 Forbidden if the test fails
Post Delete View
1class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView): 2 """ 3 Allows authors to delete their posts. 4 """ 5 model = Post 6 success_url = reverse_lazy('blog:post-list') 7 8 def test_func(self): 9 """ 10 Only the author can delete. 11 """ 12 return self.request.user == self.get_object().author
What DeleteView does automatically:
- Displays a confirmation page
- Deletes the object on POST confirmation
- Redirects to
success_url
🔗 4. URL Configuration
Views are useless without URLs pointing to them. Django uses urls.py as the routing table.
1# blog/urls.py 2 3from django.urls import path 4from . import views 5 6# Namespace for URL reversing (e.g., {% url 'blog:home' %}) 7app_name = 'blog' 8 9urlpatterns = [ 10 # Function-Based View routes 11 path('', views.home, name='home'), 12 path('search/', views.search_posts, name='search'), 13 14 # Class-Based View routes (use .as_view() to convert class to function) 15 path('posts/', views.PostListView.as_view(), name='post-list'), 16 path('posts/<slug:slug>/', views.PostDetailView.as_view(), name='post-detail'), 17 path('posts/create/', views.PostCreateView.as_view(), name='post-create'), 18 path('posts/<slug:slug>/update/', views.PostUpdateView.as_view(), name='post-update'), 19 path('posts/<slug:slug>/delete/', views.PostDeleteView.as_view(), name='post-delete'), 20]
URL pattern syntax:
| Pattern | Captures | Example URL |
|---|---|---|
path('posts/', ...) | Static URL | /posts/ |
path('posts/<slug:slug>/', ...) | Slug string | /posts/hello-world/ |
path('posts/<int:pk>/', ...) | Integer | /posts/42/ |
path('posts/<str:category>/', ...) | String | /posts/tech/ |
Don't forget to include your app's URLs in the project:
1# myproject/urls.py 2 3from django.contrib import admin 4from django.urls import path, include 5 6urlpatterns = [ 7 path('admin/', admin.site.urls), 8 path('', include('blog.urls')), # Includes all blog URLs at root 9 path('users/', include('users.urls')), 10]
🛡️ 5. Authentication Mixins & Decorators
| Protection | FBV Usage | CBV Usage |
|---|---|---|
| Login required | @login_required | LoginRequiredMixin |
| Staff only | @staff_member_required | UserPassesTestMixin + is_staff |
| Custom test | Manual if checks | UserPassesTestMixin + test_func |
Mixin order matters! In Python, mixins must come before the base class:
1# Correct 2class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView): 3 ... 4 5# Wrong (UpdateView would be checked before authentication) 6class PostUpdateView(UpdateView, LoginRequiredMixin, UserPassesTestMixin): 7 ...
🧪 6. Practice Task — Convert FBVs to CBVs
Task Instructions
Convert the following FBVs from your project into CBVs:
home→HomeView(TemplateView or ListView)post_detail→ Keep asPostDetailView(already done above)search_posts→SearchView(ListView with customget_queryset)create_post→PostCreateView(already done above)
Solution: Search as CBV
1from django.views.generic import ListView 2 3 4class SearchView(ListView): 5 """ 6 Class-Based search view with pagination. 7 """ 8 model = Post 9 template_name = 'blog/search.html' 10 context_object_name = 'page_obj' 11 paginate_by = 10 12 13 def get_queryset(self): 14 """ 15 Filter posts based on the 'q' URL parameter. 16 """ 17 query = self.request.GET.get('q', '') 18 return Post.objects.filter( 19 status='published' 20 ).filter( 21 Q(title__icontains=query) | 22 Q(content__icontains=query) | 23 Q(tags__name__icontains=query) 24 ).distinct() 25 26 def get_context_data(self, **kwargs): 27 """ 28 Add the search query and total results to the template. 29 """ 30 context = super().get_context_data(**kwargs) 31 context['query'] = self.request.GET.get('q', '') 32 context['total_results'] = self.get_queryset().count() 33 return context
Then update urls.py:
1path('search/', views.SearchView.as_view(), name='search'),
❌ Common Mistakes & How to Fix Them
| Mistake | Error / Symptom | Solution |
|---|---|---|
Missing .as_view() | TypeError: as_view() missing | CBVs must be called as MyView.as_view() in URLs |
| Wrong mixin order | Authentication not enforced | Put mixins before the base view class |
slug_url_kwarg mismatch | 404 on detail pages | Ensure it matches the URL parameter name (<slug:slug>) |
reverse_lazy in class body | ImproperlyConfigured error | Use reverse_lazy instead of reverse in class attributes |
request.user in CBV | NameError: request is not defined | Use self.request.user inside CBV methods |
Forgetting app_name | NoReverseMatch in templates | Always set app_name = 'blog' for URL namespacing |
get_queryset returns list | AttributeError | Must return a QuerySet, not a list |
| returns None |
✅ Module 7 Summary
| Concept | Key Takeaway |
|---|---|
| View | Receives HTTP request, returns HTTP response |
| FBV | Plain Python function. Full control. Good for custom logic. |
| CBV | Python class inheriting generic views. Less code. Good for CRUD. |
render() | Renders a template with context data |
get_object_or_404() | Fetches one object or raises 404 |
@login_required | Protects FBVs from anonymous access |
LoginRequiredMixin | Protects CBVs from anonymous access |
UserPassesTestMixin | Object-level permissions in CBVs |
test_func() | Custom permission logic (author == user) |
ListView | Displays a paginated list of objects |
DetailView | Displays a single object |
CreateView | Displays a form and creates an object |
UpdateView | Displays a form and updates an object |
🚪 What's Next?
In Module 8, we will master Forms & Validation. You'll learn to create Django forms, validate user input, handle file uploads, protect against CSRF attacks, and build custom validation logic. We'll create forms for post creation and comments with server-side validation.
Before proceeding, make sure:
- Your
blog/views.pycontains both FBV and CBV examples - Your
blog/urls.pyroutes all views correctly withapp_name = 'blog' - You can access
/posts/,/posts/<slug>/, and/search/?q=django - Unauthenticated users are redirected when trying to access
/posts/create/ - Only post authors can access
/posts/<slug>/update/ - You understand when to use
select_relatedvsprefetch_relatedin views
Your views are handling requests like a pro. Ready to validate user input with forms? 📝🚀