19.6 The admin’s forms: get_form and formfield_for_dbfield
ModelAdmin makes a change form’s model form class for each request, from the names BaseModelAdmin gives less those excluded or read-only; BaseModelAdmin makes each form field from its model field and chooses the admin’s widgets. AdminForm and its Fieldset, Fieldline, AdminField and AdminReadonlyField walk the form in its fieldsets for the template, showing read-only fields from the object.
The admin must make a form for any model without a form class from the project, and one that may change between requests: a user who may only view gets nothing to edit, and a field may be read-only once the object exists. So it keeps three things apart. The fieldsets are the layout, groups of field names, named or not, that the template follows. The form class is made from those names by modelform_factory for each request, the read-only names left out: a field named read-only is not a form field, and takes no part in validation or saving. And each form field the form does not declare comes from formfield_for_dbfield, which fields_for_model calls for each model field it takes (Model forms: ModelForm and fields_for_model).
The layout: get_fieldsets and get_fields
A fieldset is a pair of a name, or None, and a dictionary: "fields", a list of names in which a tuple or a list puts several on one line, and optionally "classes", CSS classes for the group, and "description", shown under its heading unescaped. BaseModelAdmin returns the option fieldsets if it is set, else one unnamed fieldset of BaseModelAdmin. get_fields returns fields if set; else it builds a form, get_form with no fields, and returns the names of its BaseForm and then the read-only names.
So a model admin that names no fields builds its form twice for each change form: on one "GET" of Endeavour’s as nora, BaseModelAdmin ran for id, name, tonnage, home and captain, then for the last four again.
BaseModelAdmin, BaseModelAdmin and BaseModelAdmin return the options exclude, readonly_fields and prepopulated_fields. Each takes the request and the object, None on the add page, and is a hook for deciding per request. The auth application’s UserAdmin overrides get_fieldsets and get_form so that its add page has UserAdmin and UserAdmin. An inline’s form is made from the same hooks and formfield_for_dbfield, by InlineModelAdmin (Inlines: InlineModelAdmin and its formsets).
The form class: ModelAdmin.get_form
ModelAdmin takes the request, the object, change=True on a change page, and keyword arguments, in this order:
- The fields are the
fieldsargument if there is one (the change form passes its fieldsets’ names,get_fieldspassesNone), else those ofget_fieldsets, flattened byflatten_fieldsets. - The exclusions are
get_exclude’s and every name ofget_readonly_fields. - On a change page whose user may not change the object, every field is excluded too: the view-only form has no form field made from the model (The change form:
changeform_view). - The exclusions in the form’s own Meta are added only when
get_excludegivesNone. - The form is subclassed with
typeunder its own name, each read-only name it declares as a field set toNone, whichDeclarativeFieldsMetaclasstakes to drop the field. - The arguments are that subclass as the form, the fields, the exclusions or
None, and asformfield_callbackthe model admin’sformfield_for_dbfieldwith the request bound to it;get_form’s keyword arguments come last and win. Fields stillNone, with a form whose Meta names neither fields nor exclusions, becomeALL_FIELDS. modelform_factorymakes the class. AFieldErroris raised again naming the model admin’s class:fields = ["name", "tonage"]on ShipAdmin gave “Unknown field(s) (tonage) specified for Ship. Check fields/fieldsets/exclude attributes of class ShipAdmin.”
ShipAdmin names no fields, so its form has all of Ship’s but id:
# a run of the example: ShipAdmin's form for Endeavour's change form, as nora and as piet
get_fieldsets [(None, {'fields': ['name', 'tonnage', 'home', 'captain']})]
get_form ShipForm < ModelForm < ModelForm < BaseModelForm
name CharField AdminTextInputWidget
tonnage IntegerField AdminIntegerFieldWidget
home ModelChoiceField RelatedFieldWidgetWrapper(Select); links for nora: view; for piet: add, change, delete, view
captain ModelChoiceField RelatedFieldWidgetWrapper(Select); links for nora: none; for piet: none
Above ShipForm stand the subclass of the fifth step, named after ShipAdmin’s form, and that form, forms.ModelForm.
One form field: formfield_for_dbfield
BaseModelAdmin is given a model field, the request, and the keyword arguments from the form’s Meta, and decides in this order:
- A field with choices goes to
BaseModelAdmin, which gives the field’s own.formfield_for_choice_field models, with.Field .formfield AdminRadioSelectifradio_fieldsnames it: a select of some kind needs no admin widget, says the comment. - A
ForeignKeyorManyToManyFieldtakes theformfield_overridesentry for its exact class and goes toBaseModelAdminor.formfield_for_foreignkey BaseModelAdmin. Unless.formfield_for_manytomany raw_id_fieldsnames it, the widget that comes back is wrapped inRelatedFieldWidgetWrapper, given the related model admin’s four permissions for the request, or none if that model is not registered here (The admin’s widgets: related objects, raw ids and autocomplete). - Any other field takes a deep copy of the first entry of
formfield_overridesalong its class’s MRO, for itsformfield. - Otherwise the field’s
formfieldis called as it is.
The keyword arguments passed in override an entry’s, being more specific, says the comment; a field with choices never reaches formfield_overrides. BaseModelAdmin makes the entries, merging each of the class’s formfield_overrides into a copy of FORMFIELD_FOR_DBFIELD_DEFAULTS, the admin’s widgets by model field class, “rather than simply overwriting”. With formfield_overrides = {models on ShipAdmin, name kept AdminTextInputWidget beside the help text, and home offered “(no port)”, but captain, a OneToOneField, did not.
formfield_for_foreignkey and formfield_for_manytomany choose a widget only when none was passed: by get_autocomplete_fields, then raw_id_fields, then radio_fields for a foreign key, or filter_vertical and filter_horizontal for a many-to-many. The queryset, unless passed, is BaseModelAdmin’s: the related model’s default manager, ordered by the related model admin’s BaseModelAdmin and not filtered by its BaseModelAdmin; with no model admin or no ordering, None, and the model field makes its own. A many-to-many field through a model of its own gets no form field; and a multiple select other than checkboxes or autocomplete gets “Hold down “Control”, or “Command” on a Mac, to select more than one.” in its help text, as Voyage’s calls did.
Walking the form: AdminForm and the read-only fields
The change form wraps the form in an AdminForm, with the fieldsets, the prepopulated fields and the read-only names, which on a view-only form are every name of the fieldsets. It gives a Fieldset for each fieldset, which gives a Fieldline for each entry of "fields", which gives for each name on the line an AdminReadonlyField if the name is read-only, else an AdminField around the form’s BoundField (BoundField: a field in a form). django/ walks them. AdminErrorList puts the errors of a bound form and of its inline formsets in one list, whose length chooses “error” or “errors” in the note above the fieldsets.
AdminReadonlyField finds a read-only field’s label and value itself. The label is the one the form’s Meta gives, or label_for_field’s: a model field’s verbose name, or a callable’s "short_description", or else its name made readable. The value is lookup_field’s: a model field’s value, or what a callable, a method of the model admin, or an attribute or method of the model gives for the object. A value that is no field’s is shown as a boolean icon when the callable carries "boolean", and otherwise as text, escaped unless it is already HTML. A field’s value goes by the field: a many-to-many’s objects joined by commas; a foreign key’s or a one-to-one’s object as a link to its change page in this site, with no permission asked, or as its text where no such URL reverses; anything else through display_for_field, as the change list shows it (The change list’s columns: list_display and list_editable). A lookup that raises AttributeError, ValueError or ObjectDoesNotExist shows the empty value.
Fieldset is true when "collapse" occurs in the string of its classes and no name of the fieldset’s "fields" has an error, so a group that holds an error opens. The template draws a collapsible group as <details> with its name as the <summary>, and only for a group that has a name: an unnamed fieldset with "collapse" is not collapsed.
ShipAdmin with these added gives a collapsed group, and two fields read-only once a ship exists:
# harbour/fleet/admin.py: in ShipAdmin
fieldsets = [
(None, {"fields": ["name", "home"]}),
("Register", {
"classes": ["collapse"],
"description": "As entered in the port's register.",
"fields": [("tonnage", "captain")],
}),
]
def get_readonly_fields(self, request, obj=None):
# once a ship is entered, its home and tonnage are the register's
return ["home", "tonnage"] if obj else []
# a run of the example, as nora: get_form called by hand for adding and for Endeavour, then Endeavour's AdminForm walked
add: base_fields ['name', 'home', 'tonnage', 'captain'], exclude None
change: base_fields ['name', 'captain'], exclude ['home', 'tonnage']
Fieldset None classes='' is_collapsible=False
Fieldline: AdminField name
Fieldline: AdminReadonlyField home: <a href="/admin/fleet/port/1/change/">Valletta</a>
Fieldset 'Register' classes='collapse' is_collapsible=True
Fieldline: AdminReadonlyField tonnage: 366 | AdminField captain