Master Django views — function-based views (FBVs), class-based views (CBVs), generic views, mixins, authentication, form handling, and REST-style view patterns with full practical examples.
Django views handle the logic of your web application — they receive HTTP requests and return HTTP responses. Django supports two styles: Function-Based Views (FBVs) and Class-Based Views (CBVs).
Function-Based Views (FBVs)
FBVs are simple Python functions that receive a request and return a response:
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import HttpResponse, JsonResponse
from django.core.paginator import Paginator
from .models import Post, Category, Comment
from .forms import PostForm, CommentForm
def post_list(request):
"""List all published posts with optional filtering."""
posts = Post.objects.filter(status='published').select_related('author', 'category')
categories = Category.objects.all()
# Filter by category
category_slug = request.GET.get('category')
if category_slug:
category = get_object_or_404(Category, slug=category_slug)
posts = posts.filter(category=category)
# Search
query = request.GET.get('q', '')
if query:
posts = posts.filter(title__icontains=query)
# Pagination
paginator = Paginator(posts, 10) # 10 posts per page
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'categories': categories,
'query': query,
'total_count': posts.count(),
}
return render(request, 'blog/post_list.html', context)
def post_detail(request, slug):
"""Show a single post and handle comment submission."""
post = get_object_or_404(Post, slug=slug, status='published')
comments = post.comments.filter(approved=True).select_related('author')
comment_form = CommentForm()
# Increment view count
post.increment_views()
# Handle comment form submission
if request.method == 'POST':
if not request.user.is_authenticated:
messages.error(request, 'Please login to comment.')
return redirect('login')
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, 'Your comment has been submitted for review.')
return redirect('blog:post-detail', slug=slug)
context = {
'post': post,
'comments': comments,
'comment_form': comment_form,
'related_posts': Post.objects.filter(
category=post.category, status='published'
).exclude(pk=post.pk)[:3],
}
return render(request, 'blog/post_detail.html', context)
@login_required
def post_create(request):
"""Create a new blog 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 ManyToMany relationships
messages.success(request, 'Post created successfully!')
return redirect('blog:post-detail', slug=post.slug)
else:
form = PostForm()
return render(request, 'blog/post_form.html', {'form': form, 'action': 'Create'})
@login_required
def post_edit(request, slug):
"""Edit an existing blog post."""
post = get_object_or_404(Post, slug=slug)
# Ensure only the author can edit
if post.author != request.user and not request.user.is_staff:
messages.error(request, 'You do not have permission to edit this post.')
return redirect('blog:post-detail', slug=slug)
if request.method == 'POST':
form = PostForm(request.POST, request.FILES, instance=post)
if form.is_valid():
form.save()
messages.success(request, 'Post updated successfully!')
return redirect('blog:post-detail', slug=post.slug)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_form.html', {'form': form, 'post': post, 'action': 'Edit'})
@login_required
def post_delete(request, slug):
"""Delete a blog post with confirmation."""
post = get_object_or_404(Post, slug=slug, author=request.user)
if request.method == 'POST':
post.delete()
messages.success(request, 'Post deleted successfully!')
return redirect('blog:post-list')
return render(request, 'blog/post_confirm_delete.html', {'post': post})
View Decorators
from django.contrib.auth.decorators import login_required, permission_required, user_passes_test
from django.views.decorators.http import require_http_methods, require_GET, require_POST
from django.views.decorators.cache import cache_page
# Require authentication
@login_required(login_url='/accounts/login/')
def dashboard(request):
return render(request, 'dashboard.html')
# Require specific permission
@permission_required('blog.add_post', raise_exception=True)
def create_post(request):
...
# Custom user test
def is_admin(user):
return user.is_staff or user.is_superuser
@user_passes_test(is_admin, login_url='/forbidden/')
def admin_panel(request):
...
# Restrict to specific HTTP methods
@require_http_methods(["GET", "POST"])
def my_view(request):
...
@require_GET
def readonly_view(request):
...
@require_POST
def submit_form(request):
...
# Cache the view for 15 minutes
@cache_page(60 * 15)
def expensive_view(request):
...
Class-Based Views (CBVs)
CBVs provide reusable, composable view logic through Python classes:
# blog/views.py
from django.views import View
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView
)
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy
from .models import Post
# Base class-based view
class PostIndexView(View):
def get(self, request):
posts = Post.objects.filter(status='published')
return render(request, 'blog/index.html', {'posts': posts})
def post(self, request):
# Handle POST requests
return HttpResponse('POST handled')
# ListView — automatically paginates and passes context
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
ordering = ['-created_at']
def get_queryset(self):
queryset = Post.objects.filter(status='published').select_related('author', 'category')
# Filter by category
category = self.request.GET.get('category')
if category:
queryset = queryset.filter(category__slug=category)
# Search
query = self.request.GET.get('q', '')
if query:
queryset = queryset.filter(title__icontains=query)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
context['query'] = self.request.GET.get('q', '')
return context
# DetailView — automatically finds object by pk or slug
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
slug_field = 'slug'
slug_url_kwarg = 'slug'
def get_queryset(self):
return Post.objects.filter(status='published').select_related('author', 'category')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
post = self.get_object()
context['comments'] = post.comments.filter(approved=True)
context['comment_form'] = CommentForm()
context['related_posts'] = Post.objects.filter(
category=post.category, status='published'
).exclude(pk=post.pk)[:3]
return context
# CreateView — handles form display and submission
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('blog:post-list')
def form_valid(self, form):
form.instance.author = self.request.user
messages.success(self.request, 'Post created successfully!')
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['action'] = 'Create'
return context
# UpdateView — pre-populates form with existing data
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
slug_field = 'slug'
def get_queryset(self):
# Only allow author to edit their own posts
return Post.objects.filter(author=self.request.user)
def form_valid(self, form):
messages.success(self.request, 'Post updated successfully!')
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('blog:post-detail', kwargs={'slug': self.object.slug})
# DeleteView — shows confirmation page, then deletes
class PostDeleteView(LoginRequiredMixin, DeleteView):
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:post-list')
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
def form_valid(self, form):
messages.success(self.request, 'Post deleted!')
return super().form_valid(form)
URL Patterns for CBVs
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
# Function-based views
path('', views.post_list, name='post-list'),
# Class-based views
path('cbv/', views.PostListView.as_view(), name='post-list-cbv'),
path('post/<slug:slug>/', views.PostDetailView.as_view(), name='post-detail'),
path('create/', views.PostCreateView.as_view(), name='post-create'),
path('post/<slug:slug>/edit/', views.PostUpdateView.as_view(), name='post-edit'),
path('post/<slug:slug>/delete/', views.PostDeleteView.as_view(), name='post-delete'),
]
Mixins
Mixins add reusable behavior to CBVs:
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import View
class OwnerRequiredMixin(UserPassesTestMixin):
"""Mixin to ensure only the owner can access/modify an object."""
def test_func(self):
obj = self.get_object()
return obj.author == self.request.user or self.request.user.is_staff
class AjaxResponseMixin:
"""Mixin to return JSON for AJAX requests."""
def dispatch(self, request, *args, **kwargs):
self.is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
if self.is_ajax:
obj = form.save()
return JsonResponse({'success': True, 'id': obj.pk})
return super().form_valid(form)
def form_invalid(self, form):
if self.is_ajax:
return JsonResponse({'success': False, 'errors': form.errors}, status=400)
return super().form_invalid(form)
# Usage
class PostCreateView(LoginRequiredMixin, AjaxResponseMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('blog:post-list')
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostDeleteView(LoginRequiredMixin, OwnerRequiredMixin, DeleteView):
model = Post
success_url = reverse_lazy('blog:post-list')
Returning Different Response Types
from django.http import HttpResponse, JsonResponse, FileResponse, StreamingHttpResponse
# Plain HTML
def plain_view(request):
return HttpResponse('<h1>Hello</h1>', content_type='text/html')
# JSON response
def api_posts(request):
posts = Post.objects.filter(status='published').values('id', 'title', 'slug')
return JsonResponse({'posts': list(posts), 'count': posts.count()})
# JSON with status code
def api_error(request):
return JsonResponse({'error': 'Not found'}, status=404)
# File download
def download_file(request):
import os
file_path = '/path/to/document.pdf'
with open(file_path, 'rb') as fh:
response = HttpResponse(fh.read(), content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename=document.pdf'
return response
# Streaming response (for large files)
def stream_large_csv(request):
def generate():
yield 'id,title,author\n'
for post in Post.objects.values_list('id', 'title', 'author__username'):
yield f'{post[0]},{post[1]},{post[2]}\n'
response = StreamingHttpResponse(generate(), content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=posts.csv'
return response
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
def post_list(request):
all_posts = Post.objects.filter(status='published')
# Create paginator — 10 items per page
paginator = Paginator(all_posts, 10)
page_number = request.GET.get('page', 1)
try:
page_obj = paginator.page(page_number)
except PageNotAnInteger:
page_obj = paginator.page(1)
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
context = {
'page_obj': page_obj,
'paginator': paginator,
}
return render(request, 'blog/post_list.html', context)
<!-- Pagination template -->
{% if page_obj.has_other_pages %}
<nav class="pagination">
{% if page_obj.has_previous %}
<a href="?page=1">« First</a>
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
<span>Page {{ page_obj.number }} of {{ paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
<a href="?page={{ paginator.num_pages }}">Last »</a>
{% endif %}
</nav>
{% endif %}
Context Processors and Middleware
# myapp/context_processors.py
def site_settings(request):
return {
'SITE_NAME': 'My Blog',
'SITE_DESCRIPTION': 'A blog about Python',
'SOCIAL_LINKS': {
'github': 'https://github.com/wohotech',
'twitter': 'https://twitter.com/wohotech',
}
}
# Add to settings.py TEMPLATES context_processors list:
# 'myapp.context_processors.site_settings'
Summary
In this lesson, you learned:
- ✅ Function-Based Views (FBVs) with CRUD operations
- ✅ View decorators (login_required, permission_required, cache_page)
- ✅ Class-Based Views (ListView, DetailView, CreateView, UpdateView, DeleteView)
- ✅ URL patterns for CBVs
- ✅ Mixins for reusable view behavior
- ✅ Returning different response types (HTML, JSON, Files)
- ✅ Implementing pagination
- ✅ Context processors
*Next Lesson: Django REST API →*