Chapter 14 · Forms

14.11 Formsets: BaseFormSet and the management form

A formset is a set of forms of one class on one page, each under a numbered prefix, "form-0", "form-1", and counted by a hidden ManagementForm. formset_factory makes the class; BaseFormSet reads the count, makes the forms, validates them as a set, and says which were marked for deletion or given an order.

A page that asks for the berths of several ships at once repeats one form’s inputs, and the browser submits them as one flat set of strings. The server must tell the rows apart, learn how many were sent, know which stood for values the view already had, and let a blank new row stay blank. The prefixes tell the rows apart; the rest comes from numbers sent in the hidden inputs of the management form.

The class: formset_factory

formset_factory makes a formset class with type: a subclass of BaseFormSet, or of the formset class it is given, whose class attributes are its other arguments. The form class becomes BaseFormSet.form, and BaseFormSet.extra, BaseFormSet.min_num, BaseFormSet.max_num and the rest are what the methods read; the body of BaseFormSet sets none of them. The class is named for the form: "Set" is added to a name that ends in "Form", "FormSet" to any other.

# harbour/fleet/forms.py
from django.forms import formset_factory

BerthFormSet = formset_factory(BerthForm, extra=2, can_delete=True)

Unless given, extra is 1, min_num 0 and max_num 1000, DEFAULT_MAX_NUM, there, a comment says, to prevent memory exhaustion. BaseFormSet.absolute_max defaults to max_num plus 1000, and a max_num above it is a ValueError.

An instance is bound as a form is, when given data or files. Its prefix is "form" unless given another, from BaseFormSet.get_default_prefix, which an inline formset overrides; its initial is a list of dictionaries, the first forms’ initial values in order. BaseFormSet.form_renderer is the factory’s renderer or None, and is given to each form, which with none chooses its own; BaseFormSet.renderer is that one or the default, and draws the formset, its management form and its own errors.

One submission, read

A bound formset is given the whole of POST, the management form’s keys beside every form’s. BerthFormSet read two berths, the second marked for deletion, so:

Figure 14.3 How BerthFormSet reads one flat submission: the management form first, for the count; then each form, reading the keys under its own prefix; then the set as a whole.

The management form: ManagementForm

ManagementForm is a form of four hidden IntegerFields. "TOTAL_FORMS" and "INITIAL_FORMS" are required; "MIN_NUM_FORMS" and "MAX_NUM_FORMS" are not, and a comment says they are written out only for code in the browser, their submitted values unchecked: nothing compares them with the formset’s limits, but one that is not a whole number makes the management form invalid.

BaseFormSet.management_form makes it once, with the formset’s prefix: unbound, with the formset’s counts and limits as initial values; bound, from the formset’s data, and cleaned at once. A count missing or not a whole number ManagementForm.clean sets to 0, because, its comment says, when the management form is invalid the number of forms submitted is not known. Data with no management form makes no forms, and an error that names the inputs at fault:

>>> formset = BerthFormSet({"form-0-ship": "heron"})
>>> formset.non_form_errors().as_data()
[ValidationError(['ManagementForm data is missing or has been tampered with. Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. You may need to file a bug report if the issue persists.'])]

Counting the forms

A formset’s first forms are its initial forms, which stand for values the view already had; the rest are its extra forms, new and perhaps blank. Two methods count them.

count unbound bound
BaseFormSet.initial_form_count unbound the number of dictionaries in initial bound the management form’s "INITIAL_FORMS"
BaseFormSet.total_form_count unbound the larger of the initial count and min_num, plus extra; no more than max_num, unless the initial forms alone are more bound the management form’s "TOTAL_FORMS", no more than the absolute maximum

Unbound, every existing value is shown but, a comment says, no extra form beyond max_num. Bound, the count sent is taken up to the absolute maximum, a guard, a comment says, against clients that would force the server to make any number of forms. max_num alone limits no submission: three forms sent with max_num=2 make a valid formset without validate_max=True.

Making the forms: forms and _construct_form

BaseFormSet.forms makes the forms on first use and keeps them, one for each index below the total count, by BaseFormSet._construct_form. Iterating, indexing and the length of a formset are over this list, which BaseFormSet.initial_forms and BaseFormSet.extra_forms slice.

_construct_form gives form i the formset’s prefix, a hyphen and i, "form-0"; when the formset is bound, its whole data and files, of which the form reads only the keys under its prefix; the i-th dictionary of initial, if there is one; and two arguments a form alone seldom has. Keyword arguments from the hook BaseFormSet.get_form_kwargs, a copy of form_kwargs, override all of these, and BaseFormSet.add_fields then adds its fields:

Deleting and ordering: add_fields

BaseFormSet.add_fields adds fields to each form’s own copy of its class’s fields, leaving the class untouched. With can_order=True, it adds "ORDER", an IntegerField drawn by BaseFormSet.ordering_widget, a NumberInput, not required, whose initial value on an initial form is its index plus one. With can_delete=True, it adds "DELETE", a BooleanField drawn by BaseFormSet.deletion_widget, a CheckboxInput, not required: on every form, or with can_delete_extra=False on the initial forms only. A model formset adds each object’s key here too (Model formsets and inline formsets).

BaseFormSet.deleted_forms is the forms whose cleaned "DELETE" is true, an extra form that has not changed aside; it is empty when the formset is invalid or cannot delete. BaseFormSet.ordered_forms is the forms by their "ORDER", a blank one last, deleted forms and unchanged extra forms left out; it raises AttributeError when the formset is invalid or cannot order.

The empty form and "__prefix__"

BaseFormSet.empty_form is a form made afresh at each use, with the prefix "form-__prefix__", empty_permitted=True and no "required" attributes. Never bound and not among the forms, it is a pattern for a script in the browser, which copies it, puts the next index in place of "__prefix__", and, as the docstring of ManagementForm asks, adds one to the count. The admin’s inline formsets add rows so.

Validating the set: full_clean and is_valid

A formset is validated the first time its errors are asked for: BaseFormSet.errors and BaseFormSet.non_form_errors call BaseFormSet.full_clean if it has not run. In order:

  1. The formset’s own errors start as an empty list of its error_class, ErrorList by default, marked "nonform". An unbound formset stops here.
  2. An invalid management form adds the error shown above.
  3. Each form is validated, an unchanged extra form counted as empty. A form marked for deletion keeps its errors out of the list.
  4. Too many forms: more than max_num, deleted forms aside, with validate_max=True; or a "TOTAL_FORMS" above the absolute maximum, always. The message names max_num in both.
  5. Too few forms: fewer than min_num, deleted and empty forms aside, with validate_min=True.
  6. BaseFormSet.clean, an empty hook for checks across forms.

Steps four and five set deleted forms aside only when the steps before found no error: BaseFormSet.deleted_forms is empty for a formset that is not valid. An error raised at the fourth step or later replaces the formset’s own errors rather than joining them, and the steps after it do not run.

BaseFormSet.is_valid is false for an unbound formset; otherwise it asks every form not marked for deletion, without stopping at the first invalid one, and is true when they are all valid and the formset has no errors of its own. BaseFormSet.total_error_count adds those errors to the number of fields with errors in each entry of errors, a form’s "__all__" counted as one; BaseFormSet.has_changed is true when any form has changed. all_valid asks every formset in a list, all of them even after one fails; the admin’s add and change view checks its inline formsets with it.

Drawing a formset

str(formset) renders the formset’s template with the context {"formset": formset} (Rendering a form: renderers and templates). BaseFormSet.template_name is the renderer’s formset_template_name, "django/forms/formsets/div.html"; as_p and the other three take BaseFormSet.template_name_p and its siblings. Each template writes the management form, then each form by the form’s method of the same style, which draws it by the form’s template_name_div or a sibling, not by the renderer’s form_template_name. None writes the formset’s own errors or its empty form.

BaseFormSet.media and BaseFormSet.is_multipart ask the first form, or the empty form when there are none; a comment says all the forms of a formset are alike (Form assets: Media).