Search code examples
djangodjango-viewsdjango-formsinline-formsetdjango-validation

Django inlineformset_factory validators not working


Code Snippet

Django version: 3.1.7

models.py

# custom validator
is_numeric = RegexValidator(r"^[0-9]*$", "Only numbers are allowed")

class Person(models.Model):
    # all the boring fields


class PersonAddress(models.Model):
    # all the boring fields
    person = models.ForeignKey(
        Person, null=True, on_delete=models.SET_NULL, related_name="person_address",
    )
    postcode = models.CharField(
        blank=True, help_text="Include leading zero if exists",
        max_length=10, validators=[is_numeric],
    )

PersonAddress will be the inlineformset of Person as a Person can have multiple addresses

forms.py

class PersonForm(forms.ModelForm):
    class Meta:
        model = Person
        fields = "__all__"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()


class PersonAddressForm(forms.ModelForm):
    class Meta:
        model = PersonAddress
        fields = "__all__"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_method = "post"


PersonAddressFormSet = forms.models.inlineformset_factory(
    Person, PersonAddress, form=PersonAddressForm, extra=1, can_delete=True,
)

# min_num, validate_min don't help even though I read from other stackoverflow solution
# PersonAddressFormSet = forms.models.inlineformset_factory(
#     Person, PersonAddress, form=PersonAddressForm, extra=1, can_delete=True, min_num=1, validate_min=True,
# )

views.py

# Person CreateView
class PersonCreateView(SuccessMessageMixin, LoginRequiredMixin, CreateView):
    model = Person
    form_class = PersonForm

    def get_context_data(self, **kwargs):
        context = super(PersonCreateView, self).get_context_data(**kwargs)
        if self.request.POST:
            context["addresses"] = PersonAddressFormSet(self.request.POST, self.request.FILES)
        else:
            context["addresses"] = PersonAddressFormSet()
        return context

    # I think the problem is here, not 100% sure
    def form_valid(self, form):
        context = self.get_context_data()
        addresses = context["addresses"]
        self.object = form.save()
        if addresses.is_valid():
            addresses.instance = self.object
            addresses.save()

        # https://stackoverflow.com/questions/47476941/django-inlineformset-factory-error-messages-not-working
        # my attempt, got this error
        # TypeError: join() argument must be str, bytes, or os.PathLike object, not 'NoneType'
        # else:
        #    return render(self.request, self.template_name, context)

        return super(PersonCreateView, self).form_valid(form)


# PersonAddress CreateView
class PersonAddressCreateView(SuccessMessageMixin, LoginRequiredMixin, CreateView):
    model = PersonAddress
    form_class = PersonAddressForm

Problem

If I create a PersonAddress object with postcode that's non-numeric string, the PersonAddressCreateView HTML form will return validation error.

But if I do the same thing in PersonCreateView HTML form, there won't be any validation error. Person object will be created, PersonAddress won't be created as the postcode is invalid. What I am expecting is for the PersonCreateView HTML form to show me the validation error instead of saving the form.

I think the solution should have something to do with the PersonCreateView's form_valid, but I am not sure.

Thanks in advance!


Solution

  • Indeed, the issue is in your form_valid implementation. To be exact, you are just doing nothing when the PersonAddressFormSet is not valid. To fix that issue, try:

        def form_valid(self, form):
            context = self.get_context_data()
            addresses = context["addresses"]
            self.object = form.save()
            if addresses.is_valid():
                addresses.instance = self.object
                addresses.save()
            else:
                return self.form_invalid(form)
            return super(PersonCreateView, self).form_valid(form)