startup (not per request).
It returns a function. That function is called on every request.
What as_view() returns, simplified:
def view(request, *args, **kwargs):
self = OrderListView() # 1. Instantiate the class
self.setup(request, *args, **kwargs) # 2. Attach request, args, kwargs
return self.dispatch(request, *args, **kwargs) # 3. Route to handler
dispatch() looks like this:
def dispatch(self, request, *args, **kwargs):
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower()) # get(), post(), etc.
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
**Critical Notes:**
- A new instance is created per request. Instance variables are safe within a single request lifecycle.
- `dispatch()` is the first method called after `setup()`. It is the correct place for cross-cutting logic.
- `as_view()` runs once at startup. Never use it for per-request initialization.
### 2. Method Resolution Order (MRO) & Mixin Composition
Python resolves method calls using MRO. For CBVs with multiple mixins, order dictates execution:
```python
# MRO is determined left-to-right, then up the hierarchy
class MyView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
...
# MRO: MyView -> LoginRequiredMixin -> PermissionRequiredMixin -> DetailView -> View
# When dispatch() is called:
# 1. Python looks in MyView β not defined
# 2. Python looks in LoginRequiredMixin β FOUND
# LoginRequiredMixin.dispatch() checks authentication.
# If user is not logged in: redirect to login.
# If user is logged in: calls super().dispatch()
# 3. super() resolves to PermissionRequiredMixin
# Checks the required permission. Denies or calls super().
# 4. super() resolves to DetailView
# Runs the actual view logic.
# WRONG ORDER: DetailView runs before authentication is checked
class MyView(DetailView, LoginRequiredMixin): # Never do this
...
Rule: Access-control mixins always go leftmost. They must intercept dispatch() before any view logic runs.
3. The super() Contract
The most common CBV mistake is omitting super() in overridden methods, which silently breaks the inheritance chain:
# BROKEN: forgetting super() discards every other mixin's contribution
class OrganisationMixin:
def get_context_data(self, **kwargs):
context = {} # starts fresh, throws away everything else
context['organisation'] = self.request.user.organisation
return context
# Result: template receives only 'organisation', nothing else.
# No 'object', no 'page_obj', no pagination β all silently discarded.
# CORRECT: always call super() and use its return value
class OrganisationMixin:
def get_queryset(self):
qs = super().get_queryset() # get the base queryset first
return qs.filter(organisation=self.request.user.organisation)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # build the full context
context['organisation'] = self.request.user.organisation
return context
4. Generic Views: Proper Implementation
ListView: Scope the Queryset
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
class OrderListView(LoginRequiredMixin, ListView):
model = Order
template_name = 'orders/list.html'
context_object_name = 'orders'
paginate_by = 20
ordering = ['-created_at']
def get_queryset(self):
# Always call super() to respect model, ordering, etc.
qs = super().get_queryset()
return (
qs
.filter(user=self.request.user)
.select_related('user', 'shipping_address')
.prefetch_related('items__product')
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# self.object_list is the paginated queryset, already in context.
# Add extra data here.
context['pending_count'] = (
Order.objects.filter(user=self.request.user, status='pending').count()
)
return context
DetailView: Scope for Authorization
from django.views.generic import DetailView
class OrderDetailView(LoginRequiredMixin, DetailView):
model = Order
template_name = 'orders/detail.html'
context_object_name = 'order'
pk_url_kwarg = 'order_id'
def get_queryset(self):
# CRITICAL: scope to current user.
# If another user tries to access an order ID that exists
# but belongs to someone else, get_object() raises Http404.
# No if/raise needed β the queryset scope handles it.
return (
Order.objects
.filter(user=self.request.user)
.select_related('user', 'shipping_address')
.prefetch_related('items__product')
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# self.object is set before get_context_data() is called
context['can_cancel'] = self.object.status == Order.Status.PENDING
return context
5. Cross-Cutting Concerns via dispatch()
For logic that must run regardless of HTTP method (rate limiting, audit logging, feature flags), override dispatch() in a mixin:
class AuditLogMixin:
"""Log every request to this view for compliance audit."""
def dispatch(self, request, *args, **kwargs):
import logging
logger = logging.getLogger('audit')
logger.info('view_accessed', extra={
'user': request.user.id,
'method': request.method,
'path': request.path,
'view': self.__class__.__name__,
})
return super().dispatch(request, *args, **kwargs)
# Compose: audit + auth + view
class SensitiveDataView(AuditLogMixin, LoginRequiredMixin, DetailView):
model = FinancialRecord
template_name = 'records/detail.html'
# By the time get() runs: audited and authenticated.
Pitfall Guide
- Breaking the
super() Contract: Overriding any CBV hook (dispatch, get, post, get_queryset, get_context_data, get_object, form_valid) without calling super() and using its return value silently discards parent class contributions. This results in missing context variables, unscoped querysets, or broken form handling without raising exceptions.
- Incorrect Mixin Ordering (MRO Violation): Placing view logic classes (e.g.,
DetailView, ListView) before access-control mixins (LoginRequiredMixin, PermissionRequiredMixin) causes the view to execute before authentication/authorization checks. Always place security mixins leftmost.
- Misusing
as_view() for Per-Request Logic: as_view() executes once at application startup to return a callable. Any initialization placed here runs only once, not per request. Per-request setup must occur in setup(), dispatch(), or HTTP method handlers.
- Overriding Generic Hooks Without
super(): Generic views like ListView and DetailView populate context and querysets automatically. Overriding get_context_data() or get_queryset() without chaining super() breaks pagination, object resolution, and default ordering.
- Forcing CBVs for Non-Standard Control Flow: CBVs assume standard HTTP method routing. When a view requires complex branching, multiple unrelated HTTP methods, or webhook/OAuth callback logic, FBVs provide clearer, more maintainable control flow.
- Neglecting Queryset Scoping for Authorization: Relying solely on
LoginRequiredMixin without scoping get_queryset() to the current user allows IDOR (Insecure Direct Object Reference) vulnerabilities. Always filter querysets by request.user to leverage Django's automatic Http404 on unauthorized access.
Deliverables
- CBV Architecture Blueprint: A structured diagram mapping Django's CBV lifecycle, MRO resolution paths, and mixin composition patterns for scalable view design.
- Pre-Deployment CBV Validation Checklist: A 12-point audit covering
super() chain integrity, mixin ordering, queryset scoping, dispatch() usage, and FBV/CBV selection criteria.
- Configuration Templates: Production-ready scaffolds for
AuditLogMixin, OrganisationMixin, scoped ListView/DetailView implementations, and cross-cutting concern decorators ready for immediate integration.