Source code for django.forms.formsets

from django.core.exceptions import ValidationError
from django.forms import Form
from django.forms.fields import BooleanField, IntegerField
from django.forms.renderers import get_default_renderer
from django.forms.utils import ErrorList, RenderableFormMixin
from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy

__all__ = ("BaseFormSet", "formset_factory", "all_valid")

# special field names

# default minimum number of forms in a formset

# default maximum number of forms in a formset, to prevent memory exhaustion

class ManagementForm(Form):
    Keep track of how many form instances are displayed on the page. If adding
    new forms via JavaScript, you should increment the count field of this form
    as well.

    template_name = "django/forms/div.html"  # RemovedInDjango50Warning.

    TOTAL_FORMS = IntegerField(widget=HiddenInput)
    INITIAL_FORMS = IntegerField(widget=HiddenInput)
    # MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of the
    # management form, but only for the convenience of client-side code. The
    # POST value of them returned from the client is not checked.
    MIN_NUM_FORMS = IntegerField(required=False, widget=HiddenInput)
    MAX_NUM_FORMS = IntegerField(required=False, widget=HiddenInput)

    def clean(self):
        cleaned_data = super().clean()
        # When the management form is invalid, we don't know how many forms
        # were submitted.
        cleaned_data.setdefault(TOTAL_FORM_COUNT, 0)
        cleaned_data.setdefault(INITIAL_FORM_COUNT, 0)
        return cleaned_data

[docs]class BaseFormSet(RenderableFormMixin): """ A collection of instances of the same Form class. """ deletion_widget = CheckboxInput ordering_widget = NumberInput default_error_messages = { "missing_management_form": _( "ManagementForm data is missing or has been tampered with. Missing fields: " "%(field_names)s. You may need to file a bug report if the issue persists." ), "too_many_forms": ngettext_lazy( "Please submit at most %(num)d form.", "Please submit at most %(num)d forms.", "num", ), "too_few_forms": ngettext_lazy( "Please submit at least %(num)d form.", "Please submit at least %(num)d forms.", "num", ), } template_name_div = "django/forms/formsets/div.html" template_name_p = "django/forms/formsets/p.html" template_name_table = "django/forms/formsets/table.html" template_name_ul = "django/forms/formsets/ul.html" def __init__( self, data=None, files=None, auto_id="id_%s", prefix=None, initial=None, error_class=ErrorList, form_kwargs=None, error_messages=None, ): self.is_bound = data is not None or files is not None self.prefix = prefix or self.get_default_prefix() self.auto_id = auto_id = data or {} self.files = files or {} self.initial = initial self.form_kwargs = form_kwargs or {} self.error_class = error_class self._errors = None self._non_form_errors = None messages = {} for cls in reversed(type(self).__mro__): messages.update(getattr(cls, "default_error_messages", {})) if error_messages is not None: messages.update(error_messages) self.error_messages = messages def __iter__(self): """Yield the forms in the order they should be rendered.""" return iter(self.forms) def __getitem__(self, index): """Return the form at the given index, based on the rendering order.""" return self.forms[index] def __len__(self): return len(self.forms) def __bool__(self): """ Return True since all formsets have a management form which is not included in the length. """ return True def __repr__(self): if self._errors is None: is_valid = "Unknown" else: is_valid = ( self.is_bound and not self._non_form_errors and not any(form_errors for form_errors in self._errors) ) return "<%s: bound=%s valid=%s total_forms=%s>" % ( self.__class__.__qualname__, self.is_bound, is_valid, self.total_form_count(), ) @cached_property def management_form(self): """Return the ManagementForm instance for this FormSet.""" if self.is_bound: form = ManagementForm(, auto_id=self.auto_id, prefix=self.prefix, renderer=self.renderer, ) form.full_clean() else: form = ManagementForm( auto_id=self.auto_id, prefix=self.prefix, initial={ TOTAL_FORM_COUNT: self.total_form_count(), INITIAL_FORM_COUNT: self.initial_form_count(), MIN_NUM_FORM_COUNT: self.min_num, MAX_NUM_FORM_COUNT: self.max_num, }, renderer=self.renderer, ) return form def total_form_count(self): """Return the total number of forms in this FormSet.""" if self.is_bound: # return absolute_max if it is lower than the actual total form # count in the data; this is DoS protection to prevent clients # from forcing the server to instantiate arbitrary numbers of # forms return min( self.management_form.cleaned_data[TOTAL_FORM_COUNT], self.absolute_max ) else: initial_forms = self.initial_form_count() total_forms = max(initial_forms, self.min_num) + self.extra # Allow all existing related objects/inlines to be displayed, # but don't allow extra beyond max_num. if initial_forms > self.max_num >= 0: total_forms = initial_forms elif total_forms > self.max_num >= 0: total_forms = self.max_num return total_forms def initial_form_count(self): """Return the number of forms that are required in this FormSet.""" if self.is_bound: return self.management_form.cleaned_data[INITIAL_FORM_COUNT] else: # Use the length of the initial data if it's there, 0 otherwise. initial_forms = len(self.initial) if self.initial else 0 return initial_forms @cached_property def forms(self): """Instantiate forms at first property access.""" # DoS protection is included in total_form_count() return [ self._construct_form(i, **self.get_form_kwargs(i)) for i in range(self.total_form_count()) ] def get_form_kwargs(self, index): """ Return additional keyword arguments for each individual formset form. index will be None if the form being constructed is a new empty form. """ return self.form_kwargs.copy() def _construct_form(self, i, **kwargs): """Instantiate and return the i-th form instance in a formset.""" defaults = { "auto_id": self.auto_id, "prefix": self.add_prefix(i), "error_class": self.error_class, # Don't render the HTML 'required' attribute as it may cause # incorrect validation for extra, optional, and deleted # forms in the formset. "use_required_attribute": False, "renderer": self.renderer, } if self.is_bound: defaults["data"] = defaults["files"] = self.files if self.initial and "initial" not in kwargs: try: defaults["initial"] = self.initial[i] except IndexError: pass # Allow extra forms to be empty, unless they're part of # the minimum forms. if i >= self.initial_form_count() and i >= self.min_num: defaults["empty_permitted"] = True defaults.update(kwargs) form = self.form(**defaults) self.add_fields(form, i) return form @property def initial_forms(self): """Return a list of all the initial forms in this formset.""" return self.forms[: self.initial_form_count()] @property def extra_forms(self): """Return a list of all the extra forms in this formset.""" return self.forms[self.initial_form_count() :] @property def empty_form(self): form_kwargs = { **self.get_form_kwargs(None), "auto_id": self.auto_id, "prefix": self.add_prefix("__prefix__"), "empty_permitted": True, "use_required_attribute": False, "renderer": self.renderer, } form = self.form(**form_kwargs) self.add_fields(form, None) return form @property def cleaned_data(self): """ Return a list of form.cleaned_data dicts for every form in self.forms. """ if not self.is_valid(): raise AttributeError( "'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__ ) return [form.cleaned_data for form in self.forms] @property def deleted_forms(self): """Return a list of forms that have been marked for deletion.""" if not self.is_valid() or not self.can_delete: return [] # construct _deleted_form_indexes which is just a list of form indexes # that have had their deletion widget set to True if not hasattr(self, "_deleted_form_indexes"): self._deleted_form_indexes = [] for i, form in enumerate(self.forms): # if this is an extra form and hasn't changed, don't consider it if i >= self.initial_form_count() and not form.has_changed(): continue if self._should_delete_form(form): self._deleted_form_indexes.append(i) return [self.forms[i] for i in self._deleted_form_indexes] @property def ordered_forms(self): """ Return a list of form in the order specified by the incoming data. Raise an AttributeError if ordering is not allowed. """ if not self.is_valid() or not self.can_order: raise AttributeError( "'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__ ) # Construct _ordering, which is a list of (form_index, order_field_value) # tuples. After constructing this list, we'll sort it by order_field_value # so we have a way to get to the form indexes in the order specified # by the form data. if not hasattr(self, "_ordering"): self._ordering = [] for i, form in enumerate(self.forms): # if this is an extra form and hasn't changed, don't consider it if i >= self.initial_form_count() and not form.has_changed(): continue # don't add data marked for deletion to self.ordered_data if self.can_delete and self._should_delete_form(form): continue self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME])) # After we're done populating self._ordering, sort it. # A sort function to order things numerically ascending, but # None should be sorted below anything else. Allowing None as # a comparison value makes it so we can leave ordering fields # blank. def compare_ordering_key(k): if k[1] is None: return (1, 0) # +infinity, larger than any number return (0, k[1]) self._ordering.sort(key=compare_ordering_key) # Return a list of form.cleaned_data dicts in the order specified by # the form data. return [self.forms[i[0]] for i in self._ordering] @classmethod def get_default_prefix(cls): return "form"
[docs] @classmethod def get_deletion_widget(cls): return cls.deletion_widget
[docs] @classmethod def get_ordering_widget(cls): return cls.ordering_widget
def non_form_errors(self): """ Return an ErrorList of errors that aren't associated with a particular form -- i.e., from formset.clean(). Return an empty ErrorList if there are none. """ if self._non_form_errors is None: self.full_clean() return self._non_form_errors @property def errors(self): """Return a list of form.errors for every form in self.forms.""" if self._errors is None: self.full_clean() return self._errors
[docs] def total_error_count(self): """Return the number of errors across all forms in the formset.""" return len(self.non_form_errors()) + sum( len(form_errors) for form_errors in self.errors )
def _should_delete_form(self, form): """Return whether or not the form was marked for deletion.""" return form.cleaned_data.get(DELETION_FIELD_NAME, False) def is_valid(self): """Return True if every form in self.forms is valid.""" if not self.is_bound: return False # Accessing errors triggers a full clean the first time only. self.errors # List comprehension ensures is_valid() is called for all forms. # Forms due to be deleted shouldn't cause the formset to be invalid. forms_valid = all( [ form.is_valid() for form in self.forms if not (self.can_delete and self._should_delete_form(form)) ] ) return forms_valid and not self.non_form_errors() def full_clean(self): """ Clean all of and populate self._errors and self._non_form_errors. """ self._errors = [] self._non_form_errors = self.error_class( error_class="nonform", renderer=self.renderer ) empty_forms_count = 0 if not self.is_bound: # Stop further processing. return if not self.management_form.is_valid(): error = ValidationError( self.error_messages["missing_management_form"], params={ "field_names": ", ".join( self.management_form.add_prefix(field_name) for field_name in self.management_form.errors ), }, code="missing_management_form", ) self._non_form_errors.append(error) for i, form in enumerate(self.forms): # Empty forms are unchanged forms beyond those with initial data. if not form.has_changed() and i >= self.initial_form_count(): empty_forms_count += 1 # Accessing errors calls full_clean() if necessary. # _should_delete_form() requires cleaned_data. form_errors = form.errors if self.can_delete and self._should_delete_form(form): continue self._errors.append(form_errors) try: if ( self.validate_max and self.total_form_count() - len(self.deleted_forms) > self.max_num ) or self.management_form.cleaned_data[ TOTAL_FORM_COUNT ] > self.absolute_max: raise ValidationError( self.error_messages["too_many_forms"] % {"num": self.max_num}, code="too_many_forms", ) if ( self.validate_min and self.total_form_count() - len(self.deleted_forms) - empty_forms_count < self.min_num ): raise ValidationError( self.error_messages["too_few_forms"] % {"num": self.min_num}, code="too_few_forms", ) # Give self.clean() a chance to do cross-form validation. self.clean() except ValidationError as e: self._non_form_errors = self.error_class( e.error_list, error_class="nonform", renderer=self.renderer, ) def clean(self): """ Hook for doing any extra formset-wide cleaning after Form.clean() has been called on every form. Any ValidationError raised by this method will not be associated with a particular form; it will be accessible via formset.non_form_errors() """ pass def has_changed(self): """Return True if data in any form differs from initial.""" return any(form.has_changed() for form in self) def add_fields(self, form, index): """A hook for adding extra fields on to each form instance.""" initial_form_count = self.initial_form_count() if self.can_order: # Only pre-fill the ordering field for initial forms. if index is not None and index < initial_form_count: form.fields[ORDERING_FIELD_NAME] = IntegerField( label=_("Order"), initial=index + 1, required=False, widget=self.get_ordering_widget(), ) else: form.fields[ORDERING_FIELD_NAME] = IntegerField( label=_("Order"), required=False, widget=self.get_ordering_widget(), ) if self.can_delete and ( self.can_delete_extra or (index is not None and index < initial_form_count) ): form.fields[DELETION_FIELD_NAME] = BooleanField( label=_("Delete"), required=False, widget=self.get_deletion_widget(), ) def add_prefix(self, index): return "%s-%s" % (self.prefix, index) def is_multipart(self): """ Return True if the formset needs to be multipart, i.e. it has FileInput, or False otherwise. """ if self.forms: return self.forms[0].is_multipart() else: return self.empty_form.is_multipart() @property def media(self): # All the forms on a FormSet are the same, so you only need to # interrogate the first form for media. if self.forms: return self.forms[0].media else: return @property def template_name(self): return self.renderer.formset_template_name
[docs] def get_context(self): return {"formset": self}
[docs]def formset_factory( form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False, min_num=None, validate_min=False, absolute_max=None, can_delete_extra=True, renderer=None, ): """Return a FormSet for the given form class.""" if min_num is None: min_num = DEFAULT_MIN_NUM if max_num is None: max_num = DEFAULT_MAX_NUM # absolute_max is a hard limit on forms instantiated, to prevent # memory-exhaustion attacks. Default to max_num + DEFAULT_MAX_NUM # (which is 2 * DEFAULT_MAX_NUM if max_num is None in the first place). if absolute_max is None: absolute_max = max_num + DEFAULT_MAX_NUM if max_num > absolute_max: raise ValueError("'absolute_max' must be greater or equal to 'max_num'.") attrs = { "form": form, "extra": extra, "can_order": can_order, "can_delete": can_delete, "can_delete_extra": can_delete_extra, "min_num": min_num, "max_num": max_num, "absolute_max": absolute_max, "validate_min": validate_min, "validate_max": validate_max, "renderer": renderer or get_default_renderer(), } return type(form.__name__ + "FormSet", (formset,), attrs)
def all_valid(formsets): """Validate every formset and return True if all are valid.""" # List comprehension ensures is_valid() is called for all formsets. return all([formset.is_valid() for formset in formsets])
Back to Top