2

Using inlineformset_factory I am able to add / remove phone numbers related to a single customer. Only problem is, I want to require at least 1 valid phone number for each customer.

Here is some demo code:

Models:

class Customer( models.Model ):
    name = models.CharField( max_length=255 )

class PhoneNumber( models.Model ):
    customer = models.ForeignKey( Customer )
    number = models.CharField( max_length=10 )

Forms:

class CustomerForm( ModelForm ):
    class Meta:
        model = Customer
        fields = ['name']

class PhoneNumberForm( ModelForm ):
    class Meta:
        model = PhoneNumber
        fields = ['number']

Ok, so that's pretty straight forward. Then in my view:

class Create( View ):
    template_name = 'path_to_template'
    CustomerForm = forms.CustomerForm
    PhoneNumberFormSet = inlineformset_factory (
        parent_model = Customer,
        model = PhoneNumber,
        form = PhoneNumberForm,
        extra = 1,
    )

    def get(self, request):
        # Return empty forms
        context = {
            'customer_form': self.CustomerForm,
            'phone_number_formset': self.PhoneNumberFormSet
        }
        render( request, self.template_name, context)

    def post(self, request):
        this_customer_form = self.CustomerForm( request.POST )

        if this_customer_form.is_valid():
            new_customer.save(commit=False)
            this_phone_number_formset = self.PhoneNumberFormSet(request.POST, instance=new_customer)

            if this_phone_number_formset.is_valid():
                new_customer.save()
                this_phone_number_formset.save()
                return HttpResponseRedirect(reverse_lazy('customer-detail', kwargs={'pk': new_customer.pk}))

        # Something is not right, show the forms again
        this_phone_number_formset = self.PhoneNumberFormSet(request.POST)
        context = {
            'customer_form': this_customer_form,
            'phone_number_formset': this_phone_number_formset
        }
        render( request, self.template_name, context)

You get the point I think. Same thing for the Edit/Update view of the customer. Only then the forms are prepopulated.

At this point all I need is a way to require at least 1 valid PhoneNumber per Customer. I found something like:

class RequiredFormSet(BaseFormSet):
    def __init__(self, *args, **kwargs):
        super(RequiredFormSet, self).__init__(*args, **kwargs)
        for form in self.forms:
            form.empty_permitted = False

from https://stackoverflow.com/questions/2406537/django-formsets-make-first-required but it doesnt seem to work when I apply this on a BaseInlineFormSet class.

Django 1.7 seems to answer my wishes, but not for a InlineModelFormSet so far..

Any ideas?

Community
  • 1
  • 1
Daniël de Wit
  • 2,206
  • 1
  • 16
  • 13
  • you can't just make the number field on your PhoneNumber class a required field? – professorDante Feb 13 '14 at 21:46
  • @professorDante Unless you specify a model field with blank=True it will be required by default. [Django Docs](https://docs.djangoproject.com/en/dev/ref/models/fields/#blank) – Daniël de Wit Feb 14 '14 at 10:05

2 Answers2

1

If you just want to set the minimum or maximum, you can set them directly in inlineformset_factory, here's my code for minimum of one entry

from django.forms import inlineformset_factory

SubUnitFormSet = inlineformset_factory(
    Unit, SubUnit, form=SubUnitForm, min_num=1, validate_min=True, extra=0)

You need to properly handle this in your view. I'm using CBV and this is my code for your reference

class UnitCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
    permission_required = "core.add_unit"

    model = Unit
    form_class = UnitForm
    template_name = 'core/basic-info/unit_form.html'
    success_url = reverse_lazy('core:units')
    success_message = _("%(code)s was added successfully")

    def get_context_data(self, **kwargs):
        data = super(UnitCreateView, self).get_context_data(**kwargs)
        if self.request.POST:
            data['subunits'] = SubUnitFormSet(self.request.POST, )
        else:
            data['subunits'] = SubUnitFormSet()
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        subunits = context['subunits']
        with transaction.atomic():
            if subunits.is_valid():
                self.object = form.save()
                subunits.instance = self.object
                subunits.save()
            else:
                return self.render_to_response(self.get_context_data(form=form))
        return super(UnitCreateView, self).form_valid(form)
Rami Alloush
  • 2,308
  • 2
  • 27
  • 33
0

Thank you kezabella ( django irc ). Seems I found a solution by subclassing BaseInlineFormset:

class RequiredFormSet(BaseInlineFormSet):
    def clean(self):
        for form in self.initial_forms:
            if not form.is_valid() or not (self.can_delete and form.cleaned_data.get('DELETE')):
                return
        for form in self.extra_forms:
            if form.has_changed():
                return
        raise ValidationError("No initial or changed extra forms")

Btw, these validation errors do not show up in {{ formset.error }} but in:

{{ formset.non_form_errors }}
Daniël de Wit
  • 2,206
  • 1
  • 16
  • 13