In a Django UpdateView for podcast episodes guarded by UserPassesTestMixin, why does an ownership test_func load the episode twice, and how do you fix it?
answer
- dispatch() runs before get()
- self.object is not set yet
- the handler fetches again
- memoise or scope the queryset
basics
~10 sUserPassesTestMixin runs test_func() inside dispatch(), before UpdateView's get() or post() sets self.object, so test_func must call get_object() and the handler then calls it again. Memoise get_object(), or scope get_queryset() to the owner.
solid answer
~40 s`UserPassesTestMixin.dispatch()` calls `test_func()` and only then `super().dispatch()`, which routes to `BaseUpdateView.get()` or `post()`. Those handlers are where `self.object = self.get_object()` happens, so at test time there is no object. A `test_func` that returns `self.get_object().owner == self.request.user` runs one query, and the handler runs a second. Setting `self.object` inside `test_func` does not help, because the handler overwrites it with a fresh `get_object()` call. Fixes: memoise `get_object()` on the instance so both callers share one fetch, or drop the test and return `Episode.objects.filter(owner=self.request.user)` from `get_queryset()`, which turns a non-owner into a 404 in one query. Also note that two `UserPassesTestMixin` subclasses cannot be stacked: only the first `test_func` in the MRO runs.
code
python · 25 linesfrom django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic.edit import UpdateView
from .models import Episode
class EpisodeUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Episode
fields = ['title', 'show_notes']
def get_object(self, queryset=None):
if not hasattr(self, '_episode'):
self._episode = super().get_object(queryset)
return self._episode # shared by test_func() and get()/post()
def test_func(self):
return self.get_object().owner_id == self.request.user.pk
class ScopedEpisodeUpdateView(LoginRequiredMixin, UpdateView):
model = Episode
fields = ['title', 'show_notes']
def get_queryset(self):
return Episode.objects.filter(owner=self.request.user) # non-owner gets 404go deeper
Recall that UserPassesTestMixin calls test_func() before the view's get() or post() runs.
Walk the order: login check, test_func in dispatch, then the handler that sets self.object, and count the queries that follow.
Offer the two single-query fixes, explain the 403 versus 404 difference, and catch the stacked test_func mixins that silently skip a rule.
Decide a house pattern for ownership checks, one mechanism used everywhere and covered by a cross-user test, rather than a mix of test_funcs and scoped querysets.
## The order of events in a guarded UpdateView Take `class EpisodeUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView)` on a podcast hosting site, where only an episode's owner may edit it. On a request: 1. `LoginRequiredMixin.dispatch()` checks authentication, then calls `super().dispatch()`. 2. `UserPassesTestMixin.dispatch()` calls `self.get_test_func()()`, normally `test_func()`. If it returns a falsy value, `handle_no_permission()` responds; otherwise `super().dispatch()`. 3. `View.dispatch()` routes to `get()` or `post()`. 4. `BaseUpdateView.get()` / `post()` runs `self.object = self.get_object()` and continues into form handling. The ownership check needs the episode, but the episode is only loaded at step 4. ## Why the naive fix still costs two queries | `test_func` body | Queries | |---|---| | `return self.get_object().owner == self.request.user` | 2: one in the test, one in the handler | | `self.object = self.get_object(); return self.object.owner == ...` | 2: the handler reassigns `self.object` with a new `get_object()` | | memoised `get_object()` shared by both | 1 | | no `test_func`; `get_queryset()` filtered by owner | 1 | The second row is the common trap: developers assume setting `self.object` early is reused, but `BaseUpdateView` calls `get_object()` unconditionally. ## Fix 1: memoise get_object() Override `get_object()` to cache its result on the instance. Django creates a new view instance per request, so an instance attribute is request-scoped and safe. `test_func` and the handler then share one fetch, and the 403 behaviour of `UserPassesTestMixin` is kept: an authenticated non-owner gets `PermissionDenied`, an anonymous user is sent to the login page. ## Fix 2: scope the queryset Return only the user's episodes from `get_queryset()`. `get_object()` then raises `Http404` for anyone else's episode, in the same single query, and the view needs no `test_func` at all. The difference is the response: a 404 hides that the episode exists, a 403 admits it. Which one a product wants is an authorization policy question; the mixin mechanics support either. ## What each design answers The two fixes differ in more than query count. A memoised `get_object()` keeps `UserPassesTestMixin`, so an authenticated user who is not the owner gets a 403 from `handle_no_permission()`, while an anonymous user is redirected to the login page. A scoped `get_queryset()` never reaches a test: everyone except the owner sees a 404, exactly as if the episode did not exist. Both are one query; pick the response your product wants and use it on every owner-only view, so a reviewer can spot the odd one out. ## Stacking tests does not work A tempting design is one mixin per rule, such as `OwnerRequiredMixin` and `NotPublishedMixin`, each subclassing `UserPassesTestMixin` with its own `test_func`. The Django documentation warns that this does not work: - both inherit the same `UserPassesTestMixin.dispatch()`, which appears once in the MRO; - it calls `self.test_func()`, which resolves to the **first** subclass's method only; - the second rule never runs, silently. Combine rules in one `test_func`, or write your own mixins that override `dispatch()` and call `super()` cooperatively. ## Checklist for owner-only views - `LoginRequiredMixin` leftmost, so `test_func` never sees an anonymous user. - One fetch per request: memoise `get_object()` or scope `get_queryset()`. - One `test_func` per view, however many rules it checks. - A test asserting that another user's request gets the intended 403 or 404.
- Why can you not stack two UserPassesTestMixin subclasses, each with its own test_func?Both share the single `UserPassesTestMixin.dispatch()` in the MRO, and it calls `self.test_func()`, which resolves to the first subclass's method. The second rule never runs. Combine the rules in one `test_func` or write cooperative mixins that override `dispatch()` themselves.
- Is caching get_object() on self safe across requests?Yes. `as_view()` creates a new view instance for every request, so an attribute set on `self` lives only for that request. Caching on the class, or in a module-level variable, would leak one user's object into another request.
saying these in an interview costs you the question
- Setting self.object in test_func saves the handler's query
- test_func runs after get() so self.object is already available
- Stacking two UserPassesTestMixin subclasses runs both tests
- Caching the object on self leaks it between requests
- UserPassesTestMixin returns 404 when the test fails