How do I show a formset's non-form-errors inside the template?

I have a form for a job opening model + inline formset for each desired skills. Something like this:

# forms.py
class SkillInlineFormSet(forms.models.BaseInlineFormSet):
    def clean(self):
        """Do not allow Job Openings to be created without desired skills"""
        if any(self.errors):
            # Don't bother validating the formset unless each form is valid on its own
            return
        has_filled_forms = any([form.has_changed() for form in self.forms])
        if not has_filled_forms:
            raise ValidationError(
                _("You should select at least one desired skill"), code="invalid"
            )


DesiredSkillIFormSet = forms.models.inlineformset_factory(
    JobOpening
    Skill,
    formset=SkillInlineFormSet,
    fields=("skill", "is_required"),
    can_delete=False,
    extra=2,
)

Then my view is as follows:

# views.py
class TalentProfileCreate(LoginRequiredMixin, UpdateView):
    model = JobOpening
    fields = ("name", "description")
    success_url = "/"

    def form_invalid(self, form, skills_formset):
        """If any of the forms is invalid, render the invalid forms."""
        return self.render_to_response(
            self.get_context_data(
                form=form, skills_formset=skills_formset
            )
        )

    def get_object(self, queryset=None):
        try:
            return super().get_object(queryset)
        except AttributeError:
            # Treat as the new object
            return None

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["skills_formset"] = DesiredSkillIFormSet(instance=self.object)
        return context

    def get_success_message(self, created=False) -> str:
        if created:
            message = _("The profile {name} was created successfully!")
        else:
            message = _("The profile {name} was updated!")
        return message

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        # Are we creating or updating a talent profile?
        self.object = self.get_object()
        created = self.object is None

        form = self.get_form()
        form.instance.user = request.user
        skills_formset = DesiredSkillIFormSet(request.POST, instance=self.object)
        if form.is_valid() and skills_formset.is_valid():
            self.object = form.save()
            skills_formset.instance = self.object
            skills_formset.save()
            messages.success(self.request, self.get_success_message(created))
            return HttpResponseRedirect(self.get_success_url())
        else:
            return self.form_invalid(form, skills_formset)

And the template:

<!-- jobopening_form.html -->
<form method="POST">
	{% csrf_token %}
	{{ form.as_ul }}
	{{ skills_formset.management_form }}
	{{ skills_formset }}
	<input type="submit" value="Save">
</form>

The validation above works: no job openings with no desired skills return form.is_valid == False. My problem is that I can’t print that validation error to the template.

Using {skills_formset.non_form_errors} yield nothing.

However, when I print it from the views.py, I get the following:

<ul class="errorlist">
  <li>You should select at least one desired skill</li>
</ul>

Weird, right? I mean, why are there ul tags? Should I be explicitly adding that to the view’s context?

When I inspected the formset’s __dict__ method, I see that the error message is actually stored on an attribute called _non_form_errors but I can’t assess that from the template. I get the following error:

# TemplateSyntaxError at /jobopening/new
Variables and attributes may not begin with underscores: 'skills_formset._non_form_errors' 

What am I missing here? Saving and updating instances with valid entries work fine. The only thing I can’t do is the render the error message I’ve defined on forms.py.

Incidentally, when I try to access the formset’s error_message, I see a missing_management_form error (despite having it on the template). Could this be hinting to a deeper problem at play?

I’m having a hard time following your flow here, but my initial reaction is that in your form_invalid method, you’re not creating your context from the bound formset created from the POST data.
You’re calling self.get_context_data with the formset being passed to form_invalid, but in get_context_data you’re setting context['skills_formset'] to a new, unbound DesiredSkillIFormSet, which isn’t going to have the form errors associated with it.

1 Like

:exploding_head:
You’re absolutely right! I spent hours looking at that code and the answer was so simple!