Justin Paul Obsines
Justin Paul Obsines

Reputation: 97

Django InlineFormSet Validation does not raise ValidationError

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

Answers (2)

Justin Paul Obsines
Justin Paul Obsines

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

solarissmoke
solarissmoke

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

Related Questions