Django formset validation - is_valid() == True but returning an empty cleaned_data dict for last form
I have an issue where I validate a formset and when I don't supply data for a (non-required) field, it returns an empty dict. To figure out where I'm going wrong, I created a shortened version of the form, with fewer fields but exactly the same init logic. The shortened form works as expected when the field is present and removed, but the full form doesn't. Since other than the number of fields there is no difference between the forms and input data, I'm a little puzzled as to why.
For clarity - the form(s) fields are created dynamically based on initial_data or (POST) data (from kwargs) (using field_mappings dict).
class MyScaledDownForm(forms.Form):
field_mappings = {
"id": {
"field": forms.IntegerField(
help_text="",
widget=forms.HiddenInput(),
)
},
"defender": {"field": forms.CharField(help_text="", required=False)},
"sid_number": {
"field": forms.CharField(
required=False,
widget=forms.HiddenInput(),
help_text="",
)
},
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
initial_data = kwargs.get("initial")
validation_data = kwargs.get("data")
if initial_data:
for k, v in initial_data.items():
field_mapping = self.field_mappings.get(k)
if field_mapping:
self.fields[k] = field_mapping["field"]
self.fields[k].initial = v
elif validation_data:
for k, v in validation_data.items():
mk = k.split("-")[-1]
field_mapping = self.field_mappings.get(mk)
if field_mapping:
self.fields[mk] = field_mapping["field"]
else:
if args:
for k, v in args[0].items():
field_mapping = self.field_mappings.get(k)
if field_mapping:
self.fields[k] = field_mapping["field"]
elif kwargs:
for k, v in kwargs.items():
k = k.split("-")[-1]
field_mapping = self.field_mappings.get(k)
if field_mapping:
self.fields[k] = field_mapping["field"]
MyScaledDownFormSet = forms.formset_factory(
form=MyScaledDownForm,
extra=0,
)
and this is the full form with more fields, but exact same init logic:
class MyForm(forms.Form):
field_mappings = {
"id": {
"field": forms.IntegerField(
help_text="",
widget=forms.HiddenInput(),
)
},
"key_user": {
"field": forms.CharField(
help_text="",
widget=forms.HiddenInput(),
)
},
"verify_user": {
"field": forms.CharField(
help_text="", widget=forms.HiddenInput(), required=False
)
},
"sid_number": {
"field": forms.CharField(
required=False,
widget=forms.HiddenInput(),
help_text="",
)
},
"jurisdiction": {
"field": forms.ChoiceField(
choices=JURISDICTION_CHOICES,
help_text="",
)
},
"case_number": {
"field": forms.CharField(
help_text="",
)
},
"judgment_date": {
"field": forms.DateField(
required=False,
help_text="",
)
},
"type_of_transaction": {
"field": forms.CharField(
help_text="",
)
},
"transaction_date": {
"field": forms.DateField(
required=False,
help_text="",
)
},
"transaction_amount": {
"field": forms.IntegerField(
help_text="",
)
},
"defender": {"field": forms.CharField(help_text="", required=False)},
"defender_title": {"field": forms.CharField(help_text="", required=False)},
"defender_first_name": {"field": forms.CharField(help_text="", required=False)},
"defender_middle_names": {
"field": forms.CharField(help_text="", required=False)
},
"defender_last_name": {"field": forms.CharField(help_text="", required=False)},
"trading_name": {"field": forms.CharField(help_text="", required=False)},
"defender_address_line_1": {
"field": forms.CharField(
help_text="",
)
},
"defender_address_line_2": {
"field": forms.CharField(
help_text="",
)
},
"defender_address_line_3": {
"field": forms.CharField(help_text="", required=False)
},
"defender_address_line_4": {
"field": forms.CharField(help_text="", required=False)
},
"defender_address_line_5": {
"field": forms.CharField(help_text="", required=False)
},
"defender_address_postcode": {
"field": forms.CharField(help_text="", required=False)
},
"date_of_birth": {
"field": forms.DateField(
required=False,
help_text="",
)
},
"crm_case_comments": {
"field": forms.CharField(
required=False,
disabled=True,
widget=forms.Textarea(),
help_text="",
)
},
"new_comment": {"field": forms.CharField(help_text="", required=False)},
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
initial_data = kwargs.get("initial")
validation_data = kwargs.get("data")
if initial_data:
for k, v in initial_data.items():
field_mapping = self.field_mappings.get(k)
if field_mapping:
self.fields[k] = field_mapping["field"]
self.fields[k].initial = v
elif validation_data:
for k, v in validation_data.items():
mk = k.split("-")[-1]
field_mapping = self.field_mappings.get(mk)
if field_mapping:
self.fields[mk] = field_mapping["field"]
else:
if args:
for k, v in args[0].items():
field_mapping = self.field_mappings.get(k)
if field_mapping:
self.fields[k] = field_mapping["field"]
elif kwargs:
for k, v in kwargs.items():
k = k.split("-")[-1]
field_mapping = self.field_mappings.get(k)
if field_mapping:
self.fields[k] = field_mapping["field"]
MyFormSet = forms.formset_factory(
form=MyForm,
extra=0,
)
I expect that given the same data to validate, both forms will have the same outputs, but they don't.
formset_input = {
"form-TOTAL_FORMS": "2",
"form-INITIAL_FORMS": "0",
"form-0-id": 7,
"form-0-defender": "defender_zero",
"form-1-id": 8,
"form-1-defender": "defender_one",
}
scaled_down_formset = MyScaledDownFormSet(request.POST)
my_formset = self.formset(request.POST)
print(f"my_formset - {scaled_down_formset.is_valid()}, {scaled_down_formset.cleaned_data}")
print(f"formset - {my_formset.is_valid()}, {my_formset.cleaned_data}")
Output
scaled_down_formset - True, [{'id': 7, 'defender': 'defender_zero'}, {'id': 8, 'defender': 'defender_one'}]
my_formset - True, [{'id': 7, 'defender': 'defender_zero'}, {'id': 8, 'defender': 'defender_one'}]
So that's fine, and works as expected. Now, when I remove "defender" fields from the input, only one form works as expected:
formset_input = {
"form-TOTAL_FORMS": "2",
"form-INITIAL_FORMS": "0",
"form-new_comment": "Banana",
"form-0-id": 7,
# "form-0-defender": "defender_zero",
"form-1-id": 8,
# "form-1-defender": "defender_one",
}
scaled_down_formset = MyScaledDownFormSet(request.POST)
my_formset = self.formset(request.POST)
print(f"my_formset - {scaled_down_formset.is_valid()}, {scaled_down_formset.cleaned_data}")
print(f"formset - {my_formset.is_valid()}, {my_formset.cleaned_data}")
Output
scaled_down_formset - True, [{'id': 7}, {'id': 8}] # cleaned data returns both dicts
my_formset - True, [{'id': 7}, {}] # cleaned data returns only the first dict