Hello,
I’ve got an issue with formsets that seems sporadic and I’ve tried everything I can think of.
In my app, there are two different forms (Medication, HouseholdMember) that are implemented (I think) the exact same way, but when I use them, the Medication form submits fine and the HouseholdMember form gives a validation error.
For both forms, I am using a formset and django-crispy-forms, and I’m (for sake of this example) only displaying two rows in the formset.
Medication: If I fill out the first row of the formset, leaving the second row completely empty, it submits fine.
HouseholdMember: If I fill out the first row of the formset, leaving the second row completely empty, it gives an error:
[{}, {'full_name': ['This field is required.']}]
If you look over my models.py form below, you will see there is no difference in the “required fields” of the Medication and HouseholdMember models. So, I’m stuck on why the HouseholdMember formset gives an error when submitting that the “full_name” field is required (presumably for the second row). It doesn’t look any different than the Medication model’s “medication” field.
If there is any direction you can point me in the further troubleshoot this, I would greatly appreciate it!
models.py
class HouseholdMember(TheBaseClass):
history = HistoricalRecords()
client = models.ForeignKey(Client, on_delete=models.CASCADE)
full_name = models.CharField(max_length=100, default='')
relationship = models.CharField(max_length=50, choices=Relationship.choices, default='')
age = models.SmallIntegerField(blank=True, null=True)
member_info = models.TextField(blank=True, default='')
app_client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='app_client_householdmember_set', blank=True, null=True)
class Meta:
verbose_name = "Household Member"
verbose_name_plural = "Household Members"
def __str__(self):
return str(self.full_name)
class Medication(TheBaseClass):
history = HistoricalRecords()
client = models.ForeignKey(Client, on_delete=models.CASCADE)
medication = models.CharField(max_length=100, default='')
reason = models.TextField(blank=True, default='')
dosage_frequency = models.CharField(max_length=100, blank=True, default='')
prescribed_by = models.CharField(max_length=100, blank=True, default='')
compliance = models.CharField(max_length=100, blank=True, default='')
past_current = models.CharField(max_length=50, choices=PastCurrent.choices, blank=True, default='')
status = models.CharField(max_length=50, choices=MedicationStatus.choices, blank=True, default='')
class Meta:
verbose_name = "Medication"
verbose_name_plural = "Medications"
def __str__(self):
return str(self.medication)
views.py
def householdmembers_add(request, client_id):
formset = HouseholdMemberFormset(request.POST or None)
helper = HouseholdMemberFormsetHelper()
cancel_url = reverse('clients:detail', args=[client_id])
client = Client.objects.get(id=client_id)
if request.method == "POST":
if formset.is_valid():
formset.instance = client
formset.save()
messages.success(request, 'Household members successfully added!')
return redirect('clients:detail', client_id)
else:
messages.error(request, mark_safe('Error adding new household members.<br/>' + str(formset.errors)))
return redirect('clients:householdmembers_add', client_id)
context = {
"page_width": "full", # added because this is a big horizontal table; leave off for regular size template layout
"formset": formset,
"helper": helper,
"cancel_url": cancel_url,
"client_full_name": client.full_name,
"obj_name": "Household Members"
}
return render(request, 'generic_add.html', context)
def medications_add(request, client_id):
formset = MedicationFormset(request.POST or None)
helper = MedicationFormsetHelper()
cancel_url = reverse('clients:detail', args=[client_id])
client = Client.objects.get(id=client_id)
if request.method == "POST":
if formset.is_valid():
formset.instance = client
formset.save()
messages.success(request, 'Medications successfully added!')
return redirect('clients:detail', client_id)
else:
messages.error(request, mark_safe('Error adding new medications.<br/>' + str(formset.errors)))
return redirect('clients:medications_add', client_id)
context = {
"page_width": "full", # added because this is a big horizontal table; leave off for regular size template layout
"formset": formset,
"helper": helper,
"cancel_url": cancel_url,
"client_full_name": client.full_name,
"obj_name": "Medications"
}
return render(request, 'generic_add.html', context)
forms.py
class HouseholdMemberForm(ModelForm):
class Meta:
model = HouseholdMember
exclude = ['client']
widgets = {
'member_info': widgets.Textarea(attrs={'rows': '3'})
}
app_client = ClientModelChoiceField(label="App Client Record", queryset=Client.objects.all(), required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset('',
'full_name',
'relationship',
'age',
'member_info',
'app_client',
),
Hidden('hidden_client_id', '{{ c_id }}'),
ButtonHolder(
Submit('submit', 'Save'),
HTML("<a href='{{ view.get_success_url }}' class='btn btn-light'>Cancel</a>")
)
)
class HouseholdMemberFormsetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_method = 'post'
# Hide form labels just for Formset (multi-row) layout
self.form_show_labels = False
self.layout = Layout(
HTML("""
<table class='table mb-5'>
<tr>
<th style='text-align: center;'>Full Name</th>
<th style='text-align: center;'>Relationship</th>
<th style='text-align: center;'>Age</th>
<th style='text-align: center;'>Member Info</th>
<th style='text-align: center;'>App Client Record</th>
</tr>
<tr>
<td>
"""),
Field('full_name'),
HTML("""
</td>
<td>
"""),
Field('relationship'),
HTML("""
</td>
<td>
"""),
Field('age'),
HTML("""
</td>
<td>
"""),
Field('member_info'),
HTML("""
</td>
<td>
"""),
Field('app_client'),
HTML("""
</td>
</tr>
</table>
"""),
)
self.add_input(Submit("submit", "Save Multiple Entries"))
# Cancel action handled on the generic_add.html page JS
self.add_input(Button("cancelFormset", "Cancel"))
HouseholdMemberFormset = inlineformset_factory(
Client,
HouseholdMember,
form=HouseholdMemberForm,
fk_name='client',
min_num=1, # minimum number of forms that must be filled in
extra=1, # number of empty forms to display
# can_delete=False # show a checkbox in each form to delete the row
)
class MedicationForm(ModelForm):
class Meta:
model = Medication
exclude = ['client']
widgets = {
'reason': widgets.Textarea(attrs={'rows': '3'})
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset('',
'medication',
'reason',
'dosage_frequency',
'prescribed_by',
'compliance',
'past_current',
'status',
),
Hidden('hidden_client_id', '{{ c_id }}'),
ButtonHolder(
Submit('submit', 'Save'),
HTML("<a href='{{ view.get_success_url }}' class='btn btn-light'>Cancel</a>")
)
)
class MedicationFormsetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_method = 'post'
# Hide form labels just for Formset (multi-row) layout
self.form_show_labels = False
self.layout = Layout(
HTML("""
<table class='table mb-5'>
<tr>
<th style='text-align: center;'>Medication</th>
<th style='text-align: center;'>Reason</th>
<th style='text-align: center;'>Dosage/Frequency</th>
<th style='text-align: center;'>Prescribed By</th>
<th style='text-align: center;'>Compliance</th>
<th style='text-align: center;'>Past/Current</th>
<th style='text-align: center;'>Status</th>
</tr>
<tr>
<td>
"""),
Field('medication'),
HTML("""
</td>
<td>
"""),
Field('reason'),
HTML("""
</td>
<td>
"""),
Field('dosage_frequency'),
HTML("""
</td>
<td>
"""),
Field('prescribed_by'),
HTML("""
</td>
<td>
"""),
Field('compliance'),
HTML("""
</td>
<td>
"""),
Field('past_current'),
HTML("""
</td>
<td>
"""),
Field('status'),
HTML("""
</td>
</tr>
</table>
"""),
)
self.add_input(Submit("submit", "Save Multiple Entries"))
# Cancel action handled on the generic_add.html page JS
self.add_input(Button("cancelFormset", "Cancel"))
MedicationFormset = inlineformset_factory(
Client,
Medication,
form=MedicationForm,
min_num=1, # minimum number of forms that must be filled in
extra=1, # number of empty forms to display
# can_delete=False # show a checkbox in each form to delete the row
)
generic_add.html
{% load crispy_forms_tags %}
<form action="" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{% if formset %}
{% crispy formset helper %}
{% else %}
{% crispy form %}
{% endif %}
</form>