19.5 The change form: changeform_view
ModelAdmin is the admin’s add page and its change page in one view. It loads the object, asks the permission, builds the model form and the inline formsets, and for a "POST" validates them and saves through a fixed order of hooks, ModelAdmin, ModelAdmin, ModelAdmin, the log, then ModelAdmin or ModelAdmin, which answer a popup or redirect by the button pressed.
An add page and a change page differ in a few places, among them whether there is an object to load, which permission is asked, what the log says, and where the user goes afterwards; ModelAdmin and ModelAdmin each call changeform_view, the first with no object’s key.
The transaction
ModelAdmin carries the CSRF check itself, as csrf_protect made a method decorator, besides the one AdminSite adds (The admin’s URLs: AdminSite and AdminSite). For any method but "GET", "HEAD", "OPTIONS" and "TRACE" it runs ModelAdmin inside atomic, on the database the router gives for writing the model; the check stands outside the block (Transactions: autocommit, atomic and savepoints).
So a save is one transaction, from the view’s first query to the row of the log, and an exception in a later hook, saving the related objects or writing the log, takes the object’s save back with it. A form that does not validate leaves the block normally, having written nothing. The page that shows its errors is a TemplateResponse, rendered after the view returns (SimpleTemplateResponse and TemplateResponse), so its select widgets query after the commit.
Before the form
ModelAdmin first reads "_to_field", the field a related object’s popup names the object by, and refuses one BaseModelAdmin does not admit, with a 400 (The change list: changelist_view and ChangeList). A "POST" carrying "_saveasnew" has its object’s key dropped: from there on it is an add, of the data posted.
An add asks BaseModelAdmin and raises PermissionDenied without it. A change loads the object with ModelAdmin, through BaseModelAdmin, and asks BaseModelAdmin; only then, when there is no object, does it redirect to the index with a warning. So a user without the permission gets a 403 for any key: nora got one for voyage 99, and for ship 99 a redirect to /admin/ with “ship with ID “99” doesn’t exist. Perhaps it was deleted?”. Ship “abc” got the same, since get_object returns None for a key its field cannot convert.
A change form then makes the action form for the actions whose location includes it, new in 6.1 (Actions: response_action and delete_selected). Last, BaseModelAdmin gives the groups of fields and ModelAdmin a model form class with the fields they name (The admin’s forms: get_form and formfield_for_dbfield).
A "POST": validated, then saved
A "POST" that carries the action form’s choice and none of "_save", "_continue" and "_addanother" goes to ModelAdmin; its one selected key must be the object’s, or the view raises BadRequest. Any other "POST" to a change page asks BaseModelAdmin, so a user who may only view gets a 403. The view binds the form to the data, with the object as its instance, and ModelAdmin makes the inlines’ formsets (Inlines: InlineModelAdmin and its formsets). If all are valid, it saves through hooks a project overrides to step in:
ModelAdmincalls.save_form BaseModelFormwith.save commit=False: the instance with the cleaned values on it, unsaved (Model forms:ModelFormandfields_for_model). It runs once the form is valid, before the formsets are asked.ModelAdmincalls the instance’s.save_model save.ModelAdmincalls the form’s.save_related save_m2m, thenModelAdmin, the formset’s.save_formset save, for each formset.ModelAdmindescribes the change as a list that the log keeps as JSON, and.construct_change_message ModelAdminor.log_addition ModelAdminwrites it to the log (The log and the history:.log_change LogEntryandhistory_view).ModelAdminor.response_add ModelAdminanswers..response_change
The session and the user were read before the transaction, when AdminSite touched the request’s user; the permissions once, then cached on the user. Validation checked the check constraint in a savepoint, since a transaction was open (Validating an instance). The log’s content type was cached by the change page’s "GET"; else log_change selects it first.
When the form or a formset is not valid, nothing is saved and the form is shown again with its errors (Validation: full_clean, cleaned_data and the errors). Nora’s tonnage of 0 got a 200 with “Please correct the error below.” and, above the fields, “Constraint “ship_has_tonnage” is violated.”; the ship kept its tonnage, and the log got no row.
A "GET", and the page
A "GET" of an add page takes the form’s initial values from ModelAdmin: every parameter of the query string, a many-to-many field’s value split at its commas. For piet, /admin/fleet/ship/add/?name=Argo&home=2&tonnage=12 opened a form with Argo, 12 and Piraeus filled in.
The page is built alike for a "GET" and a "POST" that failed. On a change page the user may not change, every field is read-only; otherwise BaseModelAdmin says which are. The form is wrapped in an AdminForm for the templates. The title is “Add”, “Change” or “View” and the model’s verbose name, “Change ship”, by whether there is an object and whether the user may change it; the context starts from AdminSite, with a caller’s extra context laid over it. ModelAdmin adds the user’s permissions and the options the buttons depend on, and returns a TemplateResponse of the template the model admin names, if any, or else of the first found of the model’s, the application’s and the admin’s change form template (The admin’s templates and media).
The answer: response_add and response_change
The answer goes by the name of the button the "POST" carries. A popup, marked with "_popup", is answered first, with a page that hands the object’s key and text to the opener (The admin’s widgets: related objects, raw ids and autocomplete). Otherwise the response adds a success message linking the object’s name to its change page, and redirects:
| the button | after an add | after a change |
|---|---|---|
"_continue" |
after an add the new object’s change page | after a change the same page |
"_addanother" |
after an add the add page again | after a change the add page |
"_saveasnew" |
after an add the new object’s change page if Model and the user may change it, else as "_save" |
after a change never met: the request is an add |
"_save", or none |
after an add Model |
after a change Model |
Both of these go to the change list, with its preserved filters, for a user who may view or change the model, and to the index otherwise.
# a run: nora posts Endeavour's form, tonnage 369, with "_addanother", then follows the redirect
302 Found
Location: /admin/fleet/ship/add/
message (success): The ship “<a href="/admin/fleet/ship/1/change/">Endeavour</a>” was changed successfully. You may add another ship below.
GET /admin/fleet/ship/add/: 403 Forbidden
The buttons a page shows are decided by submit_row, which offered nora only “Save” and “Save and continue editing”, as nora may not add ships. The view does not ask again: a "POST" with "_addanother" is saved and sent to an add page its user cannot open.
“Save as new”: save_as
ModelAdmin puts a “Save as new” button on a change page, for a user who may add, in place of “Save and add another”, and ModelAdmin repeats the buttons above the form. The view only hands them to the template, and acts on "_saveasnew" in any "POST", so what guards a copy is the add permission. With ShipAdmin’s save_as false, piet’s "POST" of Endeavour’s form with "_saveasnew" and the name Resolution made ship 3, logged an addition and redirected to /admin/fleet/ship/3/change/; nora’s got a 403.
A copy that fails validation is shown with the change page’s template and without “Save” and “Save and continue editing”, lest the interface get confusing, says the comment; with save_as true that leaves “Save as new”. Its title still says “Add ship”.
message_user
The change form sends its messages through ModelAdmin, which calls add_message (Messages: add_message, Message and MessageMiddleware). The level may be a number or a level’s name, "warning"; another name raises ValueError. The default is INFO, which since 6.0 the admin styles apart from SUCCESS, the level the change form’s success messages pass.