Reputation: 97
I'm having trouble validating the data of my InlineFormSet. What I want is to require at least one Qualification inputted in the formset. But everytime I hit the submit button with an empty Qualification, it does not raise a ValidationError.
Here's my code:
forms.py
class QualificationForm(forms.ModelForm):
class Meta:
model = Qualification
fields = ['qualification']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.disable_csrf = True
self.helper.help_text_inline = True
self.helper.label_class = 'col-sm-4'
self.helper.field_class = 'col-sm-8'
self.helper.layout = Layout(
Field('qualification')
)
class QualificationCustomInlineFormSet(forms.BaseInlineFormSet):
def clean(self):
cleaned_data = super().clean()
for form in self.forms:
qualification = cleaned_data.get('qualification', '').strip()
if not qualification:
msg = "Please enter qualification."
self.add_error('qualification', msg)
raise forms.ValidationError(msg, "error")
return cleaned_data
views.py
class JobAddView(LoginRequiredMixin, SuccessMessageMixin, FormView):
template_name = 'cepalco_website_admin/job_form.html'
form_class = forms.JobForm
success_url = reverse_lazy('cepalco_website_admin:home')
success_message = "Successfully added %(job_title)s!"
head_title = "Add new job"
title_text = head_title
description = "Enter the following job information"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['head_title'] = self.head_title
context['title_text'] = self.title_text
context['description'] = self.description
QualificationInlineFormSet = inlineformset_factory(
Job, Qualification,
form=forms.QualificationForm, formset=forms.QualificationCustomInlineFormSet,
extra=0, can_delete=False, min_num=1
)
WorkAssignmentInlineFormSet = inlineformset_factory(
Job, WorkAssignment, form=forms.WorkAssignmentForm,
extra=0, can_delete=False, min_num=1
)
context['qualification_inlineformset'] = QualificationInlineFormSet
context['work_assignment_inlineformset'] = WorkAssignmentInlineFormSet
return context
template
{% extends 'cepalco_website_admin/base_admin_main.html' %}
{% load static %}
{% load crispy_forms_tags %}
{% block head_title %}{{ head_title }} | {{ block.super }}{% endblock head_title %}
{% block head_css %}
{{ block.super }}
{% include 'cepalco_website_admin/no_asteriskfield.html' %}
{% endblock head_css %}
{% block content_main %}
<div class="title-bar">
<h1 class="title-bar-title">
<span class="d-ib">{{ title_text }}</span>
</h1>
<p class="title-bar-description">
<small>{{ description }}</small>
</p>
</div>
<div class="row">
<div class="col-md-8">
<div class="demo-form-wrapper">
<form action="{% url 'cepalco_website_admin:job_add' %}" class="form form-horizontal" id="id_jobform" method="post">
{% csrf_token %}
<div class="divider">
<div class="divider-content"><h4>Job Information</h4></div>
</div>
<!-- <legend>Job Information</legend> -->
{% crispy form %}
<div class="divider">
<div class="divider-content"><h4>Qualifications</h4></div>
</div>
<!-- <legend>Qualifications</legend> -->
<div id="id_{{ qualification_inlineformset.prefix }}">
{% crispy qualification_inlineformset qualification_inlineformset.form.helper %}
</div>
<div class="divider">
<div class="divider-content"><h4>Work Assignments</h4></div>
</div>
<!-- <legend>Work Assignments</legend> -->
<div id="id_{{ work_assignment_inlineformset.prefix }}">
{% crispy work_assignment_inlineformset work_assignment_inlineformset.form.helper %}
</div>
<div class="form-group">
<input type="submit" name="save" value="Save" class="btn btn-primary col-sm-offset-4" id="submit-id-save" />
<input type="reset" name="reset" value="Reset" class="btn btn-inverse" id="reset-id-reset" />
</div>
</form>
</div>
</div>
</div>
{% endblock content_main %}
{% block footer_javascript %}
{{ block.super }}
<script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
<script src="{% static 'js/jquery.formset.js' %}"></script>
<script type="text/javascript">
$(function() {
$('#id_{{ qualification_inlineformset.prefix }}').formset({
prefix: "{{ qualification_inlineformset.prefix }}",
formCssClass: "{{ qualification_inlineformset.prefix }}",
addText: 'Add another',
deleteText: 'Remove',
addCssClass: 'add-qualification label label-success col-sm-offset-4',
deleteCssClass: 'delete-qualification label arrow-up arrow-primary col-sm-offset-4'
})
});
</script>
<script type="text/javascript">
$(function() {
$('#id_{{ work_assignment_inlineformset.prefix }}').formset({
prefix: "{{ work_assignment_inlineformset.prefix }}",
formCssClass: "{{ work_assignment_inlineformset.prefix }}",
addText: 'Add another',
deleteText: 'Remove',
addCssClass: 'add-work-assignment label label-success col-sm-offset-4',
deleteCssClass: 'delete-work-assignment label arrow-up arrow-primary col-sm-offset-4'
})
});
</script>
{% endblock footer_javascript %}
Hope someone can help.
Thank you.
Upvotes: 0
Views: 913
Reputation: 97
I just managed to find a solution to my problem. Turns out, I didn't need to implement an InlineFormSet clean method.
Upon clicking submit, the FormView class only validates the form specified in the form_class FormView attribute. It does not automatically validate the additional inlineformsets I've added. So what I did is validate each inlineformset in the view's form_valid method
views.py
class JobAddView(LoginRequiredMixin, SuccessMessageMixin, FormView):
template_name = 'cepalco_website_admin/job_form.html'
form_class = forms.JobForm
success_url = reverse_lazy('cepalco_website_admin:home')
success_message = "Successfully added %(job_title)s!"
head_title = "Add new job"
title_text = head_title
description = "Enter the following job information"
QualificationInlineFormSet = inlineformset_factory(
Job, Qualification,
form=forms.QualificationForm,
extra=0, can_delete=False, min_num=1
)
WorkAssignmentInlineFormSet = inlineformset_factory(
Job, WorkAssignment, form=forms.WorkAssignmentForm,
extra=0, can_delete=False, min_num=1
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['head_title'] = self.head_title
context['title_text'] = self.title_text
context['description'] = self.description
context['qualification_inlineformset'] = self.QualificationInlineFormSet
context['work_assignment_inlineformset'] = self.WorkAssignmentInlineFormSet
return context
def form_valid(self, form):
qualification_formset = self.QualificationInlineFormSet(self.request.POST)
if qualification_formset.is_valid():
return super().form_valid(form)
return render(self.request, self.template_name, {
'form': form,
'head_title': self.head_title,
'title_text': self.title_text,
'description': self.description,
'qualification_inlineformset': qualification_formset,
'work_assignment_inlineformset': self.WorkAssignmentInlineFormSet
})
So even without adding a custom inlineformset clean method, the code still validates if the formset is empty and will provide an error message that this field is required.
Hope this also helps others with the same problem.
Upvotes: 0
Reputation: 31404
The logic in your formset clean()
method isn't quite right. The return value of the super()
method doesn't contained cleaned_data
- that method always returns null
. You need to inspect the cleaned data of each form individually. Something like this:
class QualificationCustomInlineFormSet(forms.BaseInlineFormSet):
def clean(self):
# The line below isn't going to work - you also don't need to call super().
# cleaned_data = super().clean()
for form in self.forms:
# use form.cleaned_data
qualification = form.cleaned_data.get('qualification', '').strip()
if not qualification:
msg = "Please enter qualification."
raise forms.ValidationError(msg, "error")
Upvotes: 1