Class-Based Views Deep Dive

Django Intermediate/Advanced — Class-Based Views Deep Dive
Django Intermediate/Advanced
Course 2 · Chapter 7 · Class-Based Views Deep Dive

🏛️ Class-Based Views Deep Dive

Course 1's Views chapter introduced the base View class and previewed that ListView/DetailView existed. This chapter delivers on that preview — Django's generic views implement entire CRUD patterns from a few class attributes, built from small, composable mixins you can combine, override, and stack with your own.

ListView

Hand-Written FBV vs ListView

Course 1's Function-Based View
def book_list(request): books = Book.objects.all() return render(request, "catalog/book_list.html", {"books": books})
ListView
from django.views.generic import ListView class BookListView(ListView): model = Book template_name = "catalog/book_list.html" context_object_name = "books" paginate_by = 20

ListView gives you pagination for free (paginate_by) — something the hand-written FBV would need entirely new code to support. Overriding get_queryset() customizes what's listed:

class InStockBookListView(ListView): model = Book template_name = "catalog/book_list.html" context_object_name = "books" def get_queryset(self): return Book.objects.filter(in_stock=True).select_related("author")

DetailView

from django.views.generic import DetailView class BookDetailView(DetailView): model = Book template_name = "catalog/book_detail.html" context_object_name = "book"
# catalog/urls.py path("<int:pk>/", BookDetailView.as_view(), name="book-detail"),

DetailView reads the primary key from the URL kwarg named pk by default (configurable via pk_url_kwarg) and calls get_object_or_404 internally — a missing book returns a proper 404 without writing that check yourself.

✏️ CreateView, UpdateView, DeleteView

These wrap the exact ModelForm pattern from Course 1's Forms chapter — is_valid()/save() handled internally:

from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.urls import reverse_lazy class BookCreateView(CreateView): model = Book fields = ["title", "description", "price", "author"] template_name = "catalog/book_form.html" success_url = reverse_lazy("book-list") class BookUpdateView(UpdateView): model = Book fields = ["title", "description", "price"] template_name = "catalog/book_form.html" success_url = reverse_lazy("book-list") class BookDeleteView(DeleteView): model = Book template_name = "catalog/book_confirm_delete.html" success_url = reverse_lazy("book-list")

fields here is the same mass-assignment allowlist as ModelForm.Meta.fields — the exact same care from the Forms chapter still applies. Overriding form_valid() is the standard customization point for anything that needs to happen right when a save succeeds:

class BookCreateView(CreateView): model = Book fields = ["title", "price", "author"] template_name = "catalog/book_form.html" success_url = reverse_lazy("book-list") def form_valid(self, form): form.instance.created_by = self.request.user # set a field before saving return super().form_valid(form) # let the parent actually save + redirect

Mixins: What Generic Views Are Built From

Every generic view above is itself assembled from small mixins — SingleObjectMixin, MultipleObjectMixin, FormMixin — combined through Python's standard multiple inheritance. You can add your own mixins to that chain the same way:

LoginRequiredMixin

The class-based equivalent of Chapter 1's @login_required decorator — added to a generic view's inheritance list, not applied as a decorator.

Method Resolution Order (MRO)

Python resolves multiple inheritance left to right — which mixin comes first in the class definition determines which one's dispatch() runs first.

Combining a Mixin With a Generic View

from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic.edit import CreateView class BookCreateView(LoginRequiredMixin, CreateView): model = Book fields = ["title", "price", "author"] template_name = "catalog/book_form.html" success_url = reverse_lazy("book-list") login_url = "/accounts/login/" # optional — falls back to LOGIN_URL setting

💻 Coding Challenges

Challenge 1: Convert an FBV to a ListView

Given Course 1's book_list function-based view, rewrite it as a BookListView(ListView) with pagination set to 10 books per page, and update the corresponding urls.py entry.

Goal: Practice the FBV-to-generic-CBV conversion and gain pagination for free.

→ Solution

Challenge 2: Customize form_valid()

Write a ReviewCreateView(CreateView) for a Review model with a reviewer foreign key, overriding form_valid() to automatically set form.instance.reviewer = self.request.user before saving — so the field never needs to appear in the form itself.

Goal: Practice the form_valid() override pattern for setting a field the user shouldn't (and can't) submit directly.

→ Solution

Challenge 3: Fix Broken Mixin Order

Given class BookCreateView(CreateView, LoginRequiredMixin): (mixin listed second), explain why this doesn't reliably enforce the login requirement, then rewrite it with the correct inheritance order.

Goal: Practice reasoning about Python's MRO in the specific context of Django mixins.

→ Solution

⚠️ Gotcha: Mixin Order Matters

LoginRequiredMixin must come before the generic view class in the inheritance list — class BookCreateView(LoginRequiredMixin, CreateView), not the reverse. Python's method resolution order checks classes left to right, so LoginRequiredMixin needs to appear first to intercept dispatch() before CreateView's own dispatch logic runs. Get the order backwards and the access check may not run at all before the view's real logic executes — a classic, quietly broken CBV mistake that "usually works in casual testing" because the symptom is an authorization check silently not firing, not a loud error.

🎯 What's Next

The final chapter of this course is about getting everything built across both courses onto a real server: Deployment — WSGI vs ASGI, static file handling, and production settings.