Search code examples
pythondjangodjango-formsformsetdjango-validation

Django custom formset validation with additional data from other forms


To validate my formset, I need to use additional data (outside of formset). How do I pass that data to validation function when it is located in BaseFormSet. Along with my formset, the request.POST also contains the country_id I need for validation. Also I need the user from session during validation. Here is my validation code bellow:

class BaseShippingTupleFormSet(BaseFormSet):

    def clean(self):
        if any(self.errors):
            return

        for form in self.forms:
            country = TranslatedCountry.objects.get(id=country_id) #  how to get country
            shippings = ShippingTuple.objects.filter(country=country, user=user) #  how to get user
            for s in shippings:
                if value_from > s.value_from and value_from < s.value_to:
                    raise forms.ValidationError(("Please enter a value that does not overlap with already existing "
                                                 "shipping tuples for the same country ("+country.translated_name+")"))

Inline comment indicate where the variables are missing (country_id and user). Any tip or direction is appreciated.

Solution

After adding the custom __init__ method like suggested in answer by @Evgeny Barbashov and fixing some other issues, here is the working solution:

def view_with_args(request, country_id, amount):
    context = {}
    user = request.user
    country = TranslatedCountry.objects.get(id=country_id)

    CustomArgsFormSet = formset_factory(ShippingTupleForm, formset=BaseShippingTupleFormSet, extra=int(amount)-1)
    if request.method == 'POST':
        formset = CustomArgsFormSet(country, user, request.POST, request.FILES)
        if formset.is_valid():
            # save validated forms
            return redirect('success')
        else:
            context["formset"] = formset
            return render(request, 'user/dashboard/view_template.html', context)
    else:
        context["formset"] = CustomArgsFormSet(country, user)
        return render(request, 'user/dashboard/view_template.html', context)

Note

Tricky thing that needs attention are arguments to base formset class with custom init! The arguments need to be passed as non-keyword (see view above) and before the request.POST and alike, since they are unnamed args. Also they need to be in correct order, custom arguments first and then request.POST etc.

If the order is wrong the __init__ method will map the arguments incorrectly and the error messages are confusing. Otherwise the python does not allow non-keyword arg after keyword arg.


Solution

  • You can simply provide a custom init method for you formset:

    class BaseShippingTupleFormSet(BaseFormSet):
        def __init__(country_id, user, *args, **kwargs):
            self._country_id = country_id
            self._user = user
            super(BaseShippingTupleFormSet, self).__init__(*args, **kwargs)
    

    and then use self._country_id and self._user in clean method