skip to content

After a valid form, how do Django's FormView, CreateView or UpdateView, and DeleteView each decide which URL to redirect to?

level: middleimportance: should knowfreq 48%

answer

  1. three different get_success_url() versions
  2. string formatting with the object
  3. model's own canonical URL
  4. compute it before the row is gone

basics

~10 s

FormView needs success_url or raises ImproperlyConfigured. CreateView and UpdateView format success_url with the saved object's fields, else call its get_absolute_url(). DeleteView formats success_url with no fallback, computed before deleting.

solid answer

~30 s

All of them redirect to `get_success_url()`, but each mixin implements it differently. `FormMixin` returns `str(self.success_url)` and raises `ImproperlyConfigured` if it is empty. `ModelFormMixin` formats `success_url` with `self.object.__dict__`, so `'/proposals/{id}/'` works, and without one it calls `self.object.get_absolute_url()`. `DeletionMixin`, which `DeleteView` uses, also formats `success_url` but has no fallback, since the object is being removed; `BaseDeleteView.form_valid()` computes the URL before calling `delete()` because deletion sets the instance's pk to `None`. For anything dynamic, such as returning to the proposal's conference, override `get_success_url()`.

code

python · 23 lines
python
from django.urls import reverse
from django.views.generic.edit import DeleteView, UpdateView

from .models import Proposal


class ProposalUpdateView(UpdateView):
    model = Proposal
    fields = ['title', 'abstract']
    # no success_url: redirects to self.object.get_absolute_url()


class ProposalDeleteView(DeleteView):
    model = Proposal
    success_url = '/conferences/{conference_id}/proposals/'  # attname, not 'conference'


class ProposalWithdrawView(DeleteView):
    model = Proposal

    def get_success_url(self):
        # runs before self.object.delete(), so the foreign key is still set
        return reverse('proposal-list', kwargs={'conference_id': self.object.conference_id})

go deeper

for a junior

Recall that editing views redirect to get_success_url(), and that model views fall back to the model's get_absolute_url().

for a middle

Explain the three implementations: FormMixin requires success_url, ModelFormMixin formats it with the object or falls back, DeletionMixin formats it with no fallback.

for a senior

Point out the ordering in DeleteView, the attname rule for placeholders, and that any user-supplied next target must be validated before redirecting.

for a principal

Standardise on get_absolute_url() as the canonical object URL so templates, the admin and views agree, and keep success URL overrides rare and reviewed.

## One hook, three implementations Every editing generic ends a successful submission with `HttpResponseRedirect(self.get_success_url())`. What `get_success_url()` does depends on which mixin supplies it: | View | Mixin providing it | With `success_url` set | Without it | |---|---|---|---| | `FormView` | `FormMixin` | `str(success_url)` | `ImproperlyConfigured` | | `CreateView`, `UpdateView` | `ModelFormMixin` | `success_url.format(**self.object.__dict__)` | `self.object.get_absolute_url()`, else `ImproperlyConfigured` | | `DeleteView` | `DeletionMixin` | `success_url.format(**self.object.__dict__)` | `ImproperlyConfigured` | ## FormView A plain form has no object to derive a URL from, so `FormMixin.get_success_url()` simply returns `success_url` as a string. The `str()` call exists because `success_url` is often a lazy object built at import time. If the attribute is empty the view raises `ImproperlyConfigured("No URL to redirect to. Provide a success_url.")`. ## CreateView and UpdateView By the time `get_success_url()` runs, `form_valid()` has saved the form into `self.object`, so the URL can depend on the row: - **Placeholders.** `success_url = '/proposals/{id}/'` is formatted with the instance's `__dict__`. That dict holds the database column attribute names, so a foreign key is available as `{conference_id}`, not `{conference}`; `{conference}` raises a `KeyError`. - **Fallback.** With no `success_url`, the view calls `self.object.get_absolute_url()`. Defining that method on the model gives every create and update page a sensible default: after submitting a proposal, show the proposal. - **No fallback left.** If the model has no `get_absolute_url()`, the resulting `AttributeError` is turned into `ImproperlyConfigured`. ## DeleteView `DeleteView` puts `DeletionMixin` first in its bases, so its `get_success_url()` wins. It formats `success_url` the same way but has no `get_absolute_url()` fallback: the object's own page will not exist after the delete. Order matters here: 1. `BaseDeleteView.post()` fetches `self.object` and validates the confirmation form. 2. `form_valid()` calls `get_success_url()` **first**. 3. Only then does it call `self.object.delete()` and return the redirect. After `Model.delete()`, Django sets the instance's primary key attribute to `None`, so a URL built afterwards from `{id}` would read `None`. Computing it first keeps placeholders like `/conferences/{conference_id}/proposals/` usable. ## Overriding get_success_url() Override the method when the target depends on more than the object's own fields: - returning to the list for the proposal's conference, using the object's foreign key and the URL name for that list; - sending reviewers and speakers to different pages based on `self.request.user`; - honouring a `next` parameter, which must be validated against allowed hosts before use, otherwise it is an open redirect. Keep the override short and return a string. If you need a URL name reversed at class-definition time, that is the job of lazy reversing, a routing topic in its own right. ## Checklist - `FormView`: always set `success_url` or override the method. - Model views: prefer `get_absolute_url()` on the model, override only for exceptions. - `DeleteView`: always set `success_url` or override; never rely on the deleted object's page.

  • Why does success_url = '/conferences/{conference}/' raise KeyError on a Django CreateView?
    `ModelFormMixin` formats the string with `self.object.__dict__`, which stores a foreign key under its column attribute, `conference_id`. The related object is not a key in that dict, so the placeholder must be `{conference_id}`.
  • Why does DeleteView compute its success URL before deleting the object?
    `Model.delete()` sets the instance's primary key attribute to `None`, and the row is gone. `BaseDeleteView.form_valid()` therefore calls `get_success_url()` first, so placeholders and overrides can still read the object's fields.

A delete redirect is like writing down a guest's room number before they check out: once they have left, the front desk record is cleared and the number is gone.

saying these in an interview costs you the question

  • DeleteView falls back to the object's get_absolute_url() after deleting
  • FormView redirects to the current URL when success_url is missing
  • success_url placeholders can use the related object name like {conference}
  • The success URL is computed after the object has been deleted
  • CreateView needs success_url even when the model has get_absolute_url()