class BaseInlineFormSet
from django.forms import BaseInlineFormSet
A formset for child objects related to a parent.
Ancestors (MRO)
- BaseInlineFormSet
- BaseModelFormSet
- BaseFormSet
- RenderableFormMixin
- RenderableMixin
- AltersData
Attributes
| Defined in | |
|---|---|
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': '', 'too_few_forms': ''}
|
BaseFormSet |
edit_only = False
|
BaseModelFormSet |
forms = <django.utils.functional.cached_property object at 0x10f03e5f0>
|
BaseFormSet |
management_form = <django.utils.functional.cached_property object at 0x10f03e580>
|
BaseFormSet |
model = None
|
BaseModelFormSet |
template_name_div = 'django/forms/formsets/div.html'
|
BaseFormSet |
template_name_p = 'django/forms/formsets/p.html'
|
BaseFormSet |
template_name_table = 'django/forms/formsets/table.html'
|
BaseFormSet |
template_name_ul = 'django/forms/formsets/ul.html'
|
BaseFormSet |
unique_fields = set()
|
BaseModelFormSet |
Properties
def
cleaned_data():
¶
BaseFormSet
Getter
@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]
def
deleted_forms():
¶
BaseFormSet
Getter
@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]
def
empty_form():
¶
BaseFormSet
Getter
@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
def
errors():
¶
BaseFormSet
Getter
@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
extra_forms():
¶
BaseFormSet
Getter
@property
def extra_forms(self):
"""Return a list of all the extra forms in this formset."""
return self.forms[self.initial_form_count() :]
def
initial_forms():
¶
BaseFormSet
Getter
@property
def initial_forms(self):
"""Return a list of all the initial forms in this formset."""
return self.forms[: self.initial_form_count()]
def
media():
¶
BaseFormSet
Getter
@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
def
ordered_forms():
¶
BaseFormSet
Getter
@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]
def
template_name():
¶
BaseFormSet
Getter
@property
def template_name(self):
return self.renderer.formset_template_name
Methods
def
_construct_form(self, i, **kwargs):
¶
BaseInlineFormSet
def _construct_form(self, i, **kwargs):
form = super()._construct_form(i, **kwargs)
if self.save_as_new:
mutable = getattr(form.data, "_mutable", None)
# Allow modifying an immutable QueryDict.
if mutable is not None:
form.data._mutable = True
# Remove the primary key from the form's data, we are only
# creating new instances
form.data[form.add_prefix(self._pk_field.name)] = None
# Remove the foreign key from the form's data
form.data[form.add_prefix(self.fk.name)] = None
if mutable is not None:
form.data._mutable = mutable
# Set the fk value here so that the form can do its validation.
fk_value = self.instance.pk
if self.fk.remote_field.field_name != self.fk.remote_field.model._meta.pk.name:
fk_value = getattr(self.instance, self.fk.remote_field.field_name)
fk_value = getattr(fk_value, "pk", fk_value)
setattr(form.instance, self.fk.attname, fk_value)
return form
BaseModelFormSet
def _construct_form(self, i, **kwargs):
pk_required = i < self.initial_form_count()
if pk_required:
if self.is_bound:
pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name)
try:
pk = self.data[pk_key]
except KeyError:
# The primary key is missing. The user may have tampered
# with POST data.
pass
else:
to_python = self._get_to_python(self.model._meta.pk)
try:
pk = to_python(pk)
except ValidationError:
# The primary key exists but is an invalid value. The
# user may have tampered with POST data.
pass
else:
kwargs["instance"] = self._existing_object(pk)
else:
kwargs["instance"] = self.get_queryset()[i]
elif self.initial_extra:
# Set initial values for extra forms
try:
kwargs["initial"] = self.initial_extra[i - self.initial_form_count()]
except IndexError:
pass
form = super()._construct_form(i, **kwargs)
if pk_required:
form.fields[self.model._meta.pk.name].required = True
return form
BaseFormSet
Instantiate and return the i-th form instance in a formset.
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
def
_existing_object(self, pk):
¶
BaseModelFormSet
def _existing_object(self, pk):
if not hasattr(self, "_object_dict"):
self._object_dict = {o.pk: o for o in self.get_queryset()}
return self._object_dict.get(pk)
def
_get_to_python(self, field):
¶
BaseModelFormSet
If the field is a related field, fetch the concrete field's (that is, the ultimate pointed-to field's) to_python.
def _get_to_python(self, field):
"""
If the field is a related field, fetch the concrete field's (that
is, the ultimate pointed-to field's) to_python.
"""
while field.remote_field is not None:
field = field.remote_field.get_related_field()
return field.to_python
def
_should_delete_form(self, form):
¶
BaseFormSet
Return whether or not the form was marked for deletion.
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
add_fields(self, form, index):
¶
BaseInlineFormSet
def add_fields(self, form, index):
super().add_fields(form, index)
if self._pk_field == self.fk:
name = self._pk_field.name
kwargs = {"pk_field": True}
else:
# The foreign key field might not be on the form, so we poke at the
# Model field to get the label, since we need that for error messages.
name = self.fk.name
kwargs = {
"label": getattr(
form.fields.get(name), "label", capfirst(self.fk.verbose_name)
)
}
# The InlineForeignKeyField assumes that the foreign key relation is
# based on the parent model's pk. If this isn't the case, set to_field
# to correctly resolve the initial form value.
if self.fk.remote_field.field_name != self.fk.remote_field.model._meta.pk.name:
kwargs["to_field"] = self.fk.remote_field.field_name
# If we're adding a new object, ignore a parent's auto-generated key
# as it will be regenerated on the save request.
if self.instance._state.adding:
if kwargs.get("to_field") is not None:
to_field = self.instance._meta.get_field(kwargs["to_field"])
else:
to_field = self.instance._meta.pk
if to_field.has_default() and (
# Don't ignore a parent's auto-generated key if it's not the
# parent model's pk and form data is provided.
to_field.attname == self.fk.remote_field.model._meta.pk.name
or not form.data
):
setattr(self.instance, to_field.attname, None)
form.fields[name] = InlineForeignKeyField(self.instance, **kwargs)
BaseModelFormSet
Add a hidden field for the object's primary key.
def add_fields(self, form, index):
"""Add a hidden field for the object's primary key."""
from django.db.models import AutoField, ForeignKey, OneToOneField
self._pk_field = pk = self.model._meta.pk
# If a pk isn't editable, then it won't be on the form, so we need to
# add it here so we can tell which object is which when we get the
# data back. Generally, pk.editable should be false, but for some
# reason, auto_created pk fields and AutoField's editable attribute is
# True, so check for that as well.
def pk_is_not_editable(pk):
return (
(not pk.editable)
or (pk.auto_created or isinstance(pk, AutoField))
or (
pk.remote_field
and pk.remote_field.parent_link
and pk_is_not_editable(pk.remote_field.model._meta.pk)
)
)
if pk_is_not_editable(pk) or pk.name not in form.fields:
if form.is_bound:
# If we're adding the related instance, ignore its primary key
# as it could be an auto-generated default which isn't actually
# in the database.
pk_value = None if form.instance._state.adding else form.instance.pk
else:
try:
if index is not None:
pk_value = self.get_queryset()[index].pk
else:
pk_value = None
except IndexError:
pk_value = None
if isinstance(pk, (ForeignKey, OneToOneField)):
qs = pk.remote_field.model._default_manager.get_queryset()
else:
qs = self.model._default_manager.get_queryset()
qs = qs.using(form.instance._state.db)
if form._meta.widgets:
widget = form._meta.widgets.get(self._pk_field.name, HiddenInput)
else:
widget = HiddenInput
form.fields[self._pk_field.name] = ModelChoiceField(
qs, initial=pk_value, required=False, widget=widget
)
super().add_fields(form, index)
BaseFormSet
A hook for adding extra fields on to each form instance.
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):
¶
BaseFormSet
def add_prefix(self, index):
return "%s-%s" % (self.prefix, index)
def
as_div(self):
¶
RenderableFormMixin
Render as <div> elements.
def as_div(self):
"""Render as <div> elements."""
return self.render(self.template_name_div)
def
as_p(self):
¶
RenderableFormMixin
Render as <p> elements.
def as_p(self):
"""Render as <p> elements."""
return self.render(self.template_name_p)
def
as_table(self):
¶
RenderableFormMixin
Render as <tr> elements excluding the surrounding <table> tag.
def as_table(self):
"""Render as <tr> elements excluding the surrounding <table> tag."""
return self.render(self.template_name_table)
def
as_ul(self):
¶
RenderableFormMixin
Render as <li> elements excluding the surrounding <ul> tag.
def as_ul(self):
"""Render as <li> elements excluding the surrounding <ul> tag."""
return self.render(self.template_name_ul)
def
clean(self):
¶
BaseModelFormSet
def clean(self):
self.validate_unique()
BaseFormSet
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()
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
delete_existing(self, obj, commit=True):
¶
BaseModelFormSet
Deletes an existing model instance.
def delete_existing(self, obj, commit=True):
"""Deletes an existing model instance."""
if commit:
obj.delete()
def
full_clean(self):
¶
BaseFormSet
Clean all of self.data and populate self._errors and 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
get_context(self):
¶
BaseFormSet
def get_context(self):
return {"formset": self}
RenderableMixin
def get_context(self):
raise NotImplementedError(
"Subclasses of RenderableMixin must provide a get_context() method."
)
def
get_date_error_message(self, date_check):
¶
BaseModelFormSet
def get_date_error_message(self, date_check):
return gettext(
"Please correct the duplicate data for %(field_name)s "
"which must be unique for the %(lookup)s in %(date_field)s."
) % {
"field_name": date_check[2],
"date_field": date_check[3],
"lookup": str(date_check[1]),
}
def
get_form_error(self):
¶
BaseModelFormSet
def get_form_error(self):
return gettext("Please correct the duplicate values below.")
def
get_form_kwargs(self, index):
¶
BaseFormSet
Return additional keyword arguments for each individual formset form. index will be None if the form being constructed is a new empty form.
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
get_queryset(self):
¶
BaseModelFormSet
def get_queryset(self):
if not hasattr(self, "_queryset"):
if self.queryset is not None:
qs = self.queryset
else:
qs = self.model._default_manager.get_queryset()
# If the queryset isn't already ordered we need to add an
# artificial ordering here to make sure that all formsets
# constructed from this queryset have the same form order.
if not qs.ordered:
qs = qs.order_by(self.model._meta.pk.name)
# Removed queryset limiting here. As per discussion re: #13023
# on django-dev, max_num should not prevent existing
# related objects/inlines from being displayed.
self._queryset = qs
return self._queryset
def
get_unique_error_message(self, unique_check):
¶
BaseInlineFormSet
def get_unique_error_message(self, unique_check):
unique_check = [field for field in unique_check if field != self.fk.name]
return super().get_unique_error_message(unique_check)
BaseModelFormSet
def get_unique_error_message(self, unique_check):
if len(unique_check) == 1:
return gettext("Please correct the duplicate data for %(field)s.") % {
"field": unique_check[0],
}
else:
return gettext(
"Please correct the duplicate data for %(field)s, which must be unique."
) % {
"field": get_text_list(unique_check, _("and")),
}
def
has_changed(self):
¶
BaseFormSet
Return True if data in any form differs from initial.
def has_changed(self):
"""Return True if data in any form differs from initial."""
return any(form.has_changed() for form in self)
def
initial_form_count(self):
¶
BaseInlineFormSet
def initial_form_count(self):
if self.save_as_new:
return 0
return super().initial_form_count()
BaseModelFormSet
Return the number of forms that are required in this FormSet.
def initial_form_count(self):
"""Return the number of forms that are required in this FormSet."""
if not self.is_bound:
return len(self.get_queryset())
return super().initial_form_count()
BaseFormSet
Return the number of forms that are required in this FormSet.
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
def
is_multipart(self):
¶
BaseFormSet
Return True if the formset needs to be multipart, i.e. it has FileInput, or False otherwise.
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()
def
is_valid(self):
¶
BaseFormSet
Return True if every form in self.forms is valid.
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
non_form_errors(self):
¶
BaseFormSet
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.
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
def
render(self, template_name=None, context=None, renderer=None):
¶
RenderableMixin
def render(self, template_name=None, context=None, renderer=None):
renderer = renderer or self.renderer
template = template_name or self.template_name
context = context or self.get_context()
return mark_safe(renderer.render(template, context))
def
save(self, commit=True):
¶
BaseModelFormSet
Save model instances for every form, adding and changing instances as necessary, and return the list of instances.
def save(self, commit=True):
"""
Save model instances for every form, adding and changing instances
as necessary, and return the list of instances.
"""
if not commit:
self.saved_forms = []
def save_m2m():
for form in self.saved_forms:
form.save_m2m()
self.save_m2m = save_m2m
if self.edit_only:
return self.save_existing_objects(commit)
else:
return self.save_existing_objects(commit) + self.save_new_objects(commit)
def
save_existing(self, form, obj, commit=True):
¶
BaseModelFormSet
Save and return an existing model instance for the given form.
def save_existing(self, form, obj, commit=True):
"""Save and return an existing model instance for the given form."""
return form.save(commit=commit)
def
save_existing_objects(self, commit=True):
¶
BaseModelFormSet
def save_existing_objects(self, commit=True):
self.changed_objects = []
self.deleted_objects = []
if not self.initial_forms:
return []
saved_instances = []
forms_to_delete = self.deleted_forms
for form in self.initial_forms:
obj = form.instance
# If the pk is None, it means either:
# 1. The object is an unexpected empty model, created by invalid
# POST data such as an object outside the formset's queryset.
# 2. The object was already deleted from the database.
if not obj._is_pk_set():
continue
if form in forms_to_delete:
self.deleted_objects.append(obj)
self.delete_existing(obj, commit=commit)
elif form.has_changed():
self.changed_objects.append((obj, form.changed_data))
saved_instances.append(self.save_existing(form, obj, commit=commit))
if not commit:
self.saved_forms.append(form)
return saved_instances
def
save_new(self, form, commit=True):
¶
BaseInlineFormSet
def save_new(self, form, commit=True):
# Ensure the latest copy of the related instance is present on each
# form (it may have been saved after the formset was originally
# instantiated).
setattr(form.instance, self.fk.name, self.instance)
return super().save_new(form, commit=commit)
BaseModelFormSet
Save and return a new model instance for the given form.
def save_new(self, form, commit=True):
"""Save and return a new model instance for the given form."""
return form.save(commit=commit)
def
save_new_objects(self, commit=True):
¶
BaseModelFormSet
def save_new_objects(self, commit=True):
self.new_objects = []
for form in self.extra_forms:
if not form.has_changed():
continue
# If someone has marked an add form for deletion, don't save the
# object.
if self.can_delete and self._should_delete_form(form):
continue
self.new_objects.append(self.save_new(form, commit=commit))
if not commit:
self.saved_forms.append(form)
return self.new_objects
def
total_error_count(self):
¶
BaseFormSet
Return the number of errors across all forms in the formset.
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
total_form_count(self):
¶
BaseFormSet
Return the total number of forms in this FormSet.
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
validate_unique(self):
¶
BaseModelFormSet
def validate_unique(self):
# Collect unique_checks and date_checks to run from all the forms.
all_unique_checks = set()
all_date_checks = set()
forms_to_delete = self.deleted_forms
valid_forms = [
form
for form in self.forms
if form.is_valid() and form not in forms_to_delete
]
for form in valid_forms:
exclude = form._get_validation_exclusions()
unique_checks, date_checks = form.instance._get_unique_checks(
exclude=exclude,
include_meta_constraints=True,
)
all_unique_checks.update(unique_checks)
all_date_checks.update(date_checks)
errors = []
# Do each of the unique checks (unique and unique_together)
for uclass, unique_check in all_unique_checks:
seen_data = set()
for form in valid_forms:
# Get the data for the set of fields that must be unique among
# the forms.
row_data = (
field if field in self.unique_fields else form.cleaned_data[field]
for field in unique_check
if field in form.cleaned_data
)
# Reduce Model instances to their primary key values
row_data = tuple(
(
d._get_pk_val()
if hasattr(d, "_get_pk_val")
# Prevent "unhashable type" errors later on.
else make_hashable(d)
)
for d in row_data
)
if row_data and None not in row_data:
# if we've already seen it then we have a uniqueness failure
if row_data in seen_data:
# poke error messages into the right places and mark
# the form as invalid
errors.append(self.get_unique_error_message(unique_check))
form._errors[NON_FIELD_ERRORS] = self.error_class(
[self.get_form_error()],
renderer=self.renderer,
)
# Remove the data from the cleaned_data dict since it
# was invalid.
for field in unique_check:
if field in form.cleaned_data:
del form.cleaned_data[field]
# mark the data as seen
seen_data.add(row_data)
# iterate over each of the date checks now
for date_check in all_date_checks:
seen_data = set()
uclass, lookup, field, unique_for = date_check
for form in valid_forms:
# see if we have data for both fields
if (
form.cleaned_data
and form.cleaned_data[field] is not None
and form.cleaned_data[unique_for] is not None
):
# if it's a date lookup we need to get the data for all the fields
if lookup == "date":
date = form.cleaned_data[unique_for]
date_data = (date.year, date.month, date.day)
# otherwise it's just the attribute on the date/datetime
# object
else:
date_data = (getattr(form.cleaned_data[unique_for], lookup),)
data = (form.cleaned_data[field],) + date_data
# if we've already seen it then we have a uniqueness failure
if data in seen_data:
# poke error messages into the right places and mark
# the form as invalid
errors.append(self.get_date_error_message(date_check))
form._errors[NON_FIELD_ERRORS] = self.error_class(
[self.get_form_error()],
renderer=self.renderer,
)
# Remove the data from the cleaned_data dict since it
# was invalid.
del form.cleaned_data[field]
# mark the data as seen
seen_data.add(data)
if errors:
raise ValidationError(errors)