123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579 |
- 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
- TOTAL_FORM_COUNT = "TOTAL_FORMS"
- INITIAL_FORM_COUNT = "INITIAL_FORMS"
- MIN_NUM_FORM_COUNT = "MIN_NUM_FORMS"
- MAX_NUM_FORM_COUNT = "MAX_NUM_FORMS"
- ORDERING_FIELD_NAME = "ORDER"
- DELETION_FIELD_NAME = "DELETE"
- # default minimum number of forms in a formset
- DEFAULT_MIN_NUM = 0
- # default maximum number of forms in a formset, to prevent memory exhaustion
- DEFAULT_MAX_NUM = 1000
- 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.
- """
- 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
- 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
- self.data = 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
- self.form_renderer = self.renderer
- self.renderer = self.renderer or get_default_renderer()
- 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(
- self.data,
- 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.form_renderer,
- }
- if self.is_bound:
- defaults["data"] = self.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.form_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"
- @classmethod
- def get_deletion_widget(cls):
- return cls.deletion_widget
- @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
- 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 self.data 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 self.empty_form.media
- @property
- def template_name(self):
- return self.renderer.formset_template_name
- def get_context(self):
- return {"formset": self}
- 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,
- }
- 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])
|