skip to content

In a Django CreateView for talk proposals, how do you set the speaker from request.user without exposing it as a form field?

level: middleimportance: must knowfreq 70%

answer

  1. leave it out of the form
  2. hook that runs after is_valid
  3. form.instance before saving
  4. then hand back to super()

basics

~10 s

Leave speaker out of fields and override form_valid(): set form.instance.speaker = self.request.user, then return super().form_valid(form), which saves the form, stores self.object and redirects to the success URL.

solid answer

~30 s

On POST, `ProcessFormView.post()` builds the form and calls `form_valid(form)` if `is_valid()` passes, otherwise `form_invalid(form)`. So I keep `speaker` out of `fields` and override `form_valid()`: set `form.instance.speaker = self.request.user`, then `return super().form_valid(form)`. `ModelFormMixin.form_valid()` calls `form.save()`, assigns the result to `self.object`, and `FormMixin.form_valid()` returns a redirect to `get_success_url()`. The value comes from the server, so a user cannot tamper with it the way they could with a hidden input. `form_invalid()` re-renders the template with the bound form and its errors, with a 200 status, not a redirect.

code

python · 15 lines
python
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView

from .models import Proposal


class ProposalCreateView(LoginRequiredMixin, CreateView):
    model = Proposal
    fields = ['title', 'abstract', 'track']
    success_url = '/proposals/{id}/'

    def form_valid(self, form):
        form.instance.speaker = self.request.user
        form.instance.conference_id = self.kwargs['conference_id']
        return super().form_valid(form)

go deeper

for a junior

Recall the override: set form.instance.speaker from self.request.user inside form_valid, then return super().form_valid(form).

for a middle

Walk the POST path: get_form, is_valid, form_valid saving and redirecting, form_invalid re-rendering the bound form with a 200.

for a senior

Show why server-owned columns never ride in the form, and what breaks when you save by hand but forget save_m2m or self.object.

for a principal

Set a rule that ownership and tenancy columns are assigned in one server-side hook per view, reviewed like authorization code, never via hidden inputs.

## The POST path through a CreateView A `CreateView` handles a submission in a fixed order. Knowing it tells you which hook to override: 1. `BaseCreateView.post()` sets `self.object = None` (there is no object yet). 2. `ProcessFormView.post()` calls `self.get_form()`, which instantiates the form class with `get_form_kwargs()` (including `data=request.POST`, `files=request.FILES` and `instance=None`). 3. If `form.is_valid()` is true it returns `self.form_valid(form)`; otherwise `self.form_invalid(form)`. 4. `ModelFormMixin.form_valid()` runs `self.object = form.save()` and then calls `FormMixin.form_valid()`. 5. `FormMixin.form_valid()` returns `HttpResponseRedirect(self.get_success_url())`. `form_valid()` is therefore the single place where you know the input is clean and the object has not been written yet. ## Setting server-side values A talk proposal belongs to the logged-in speaker. That value must never come from the browser: - **Do not** add `speaker` to `fields` and hide it with a `HiddenInput` widget; any user can edit the HTML and submit someone else's id. - **Do** leave it out of `fields` and set it on the unsaved model instance that the `ModelForm` wraps: `form.instance.speaker = self.request.user`. - Then call `super().form_valid(form)` so the normal save, `self.object` assignment and redirect still happen. The same pattern fills other server-owned columns: the conference a proposal is submitted to (from a URL keyword argument in `self.kwargs`), a status of "submitted", or a timestamp. ## Saving by hand instead Some code uses `obj = form.save(commit=False)`, sets fields, calls `obj.save()` and then `return super().form_valid(form)`. That works but is wasteful: the parent calls `form.save()` again, which runs a second `save()` on the same instance (an extra `UPDATE`, not a duplicate row, because the primary key is now set). If you save by hand, finish by hand: - call `form.save_m2m()` if the form has many-to-many fields such as co-speakers or tags; - set `self.object = obj`; - return `HttpResponseRedirect(self.get_success_url())`. Setting `form.instance` and delegating to `super()` avoids all three steps, which is why it is the pattern Django's own documentation shows. ## What form_invalid does | Hook | Called when | Default behaviour | |---|---|---| | `form_valid(form)` | `is_valid()` is true | save (model views), then redirect to `get_success_url()` | | `form_invalid(form)` | `is_valid()` is false | `render_to_response(get_context_data(form=form))` | The invalid path re-renders the same template with the **bound** form, so the user sees their input and the error messages. The response status is 200, because `TemplateResponseMixin` does not change it. Override `form_invalid()` when you need something else, for example a 422 status for an HTMX partial or extra context. ## Common mistakes - Forgetting to `return` the result of `super().form_valid(form)`, so the view returns `None` and Django raises an error about a view not returning an `HttpResponse`. - Putting access control in `form_valid()` alone: the GET page still renders for anonymous users. Guard the whole view with an access mixin instead. - Writing the owner in `get_form_kwargs()` as `initial`; initial values are ignored once the form is bound to POST data, and they are editable anyway.

  • What changes if you call form.save(commit=False) and obj.save() yourself before returning super().form_valid(form)?
    The parent calls `form.save()` again, so the instance is saved twice: one `INSERT` then an `UPDATE` of the same row. No duplicate appears, but it is a wasted query. Either set `form.instance` and delegate, or finish manually with `save_m2m()`, `self.object = obj` and your own redirect.
  • Why does form_invalid() return a 200 instead of a redirect?
    The browser needs the bound form back, with the typed values and the errors, and a redirect would lose them. `FormMixin.form_invalid()` renders the template directly; override it if a client such as an HTMX request expects a 4xx status.

saying these in an interview costs you the question

  • Put speaker in fields and hide it with a HiddenInput widget
  • Calling super().form_valid after saving yourself inserts a duplicate row
  • form_valid runs before the form is validated
  • form_invalid redirects back to the empty form
  • Pass the user as initial data so the form fills it in