5

I have the following code:

from django import forms
from django.core.exceptions import ValidationError

class MyAdminForm(forms.ModelForm):
    class Meta:
        model = MyModel

    def clean(self):
        cleaned_data = self.cleaned_data
        max_num_items = cleaned_data['max_num_items']
        inline_items = cleaned_data.get('inlineitem_set', [])

        if len(inline_items) < 2:
            raise ValidationError('There must be at least 2 valid inline items')

        if max_num_items > len(inline_items):
            raise ValidationError('The maximum number of items must match the number of inline items there are')

        return cleaned_data

I thought I could access the formset from the cleaned_data (by using cleaned_data['inlineitem_set']) but that doesn't seem to be the case.

My questions are:

  1. How do I access the formset?
  2. Do I need to create a custom formset with my custom validation for this to work?
  3. If I need to do that, how do I access the "parent" form of the formset in its clean method?
  • I feel sorry I cannot upvote the question but I am unable to do so because it is answered but the answer is not accepted, yet. – raratiru Nov 13 '17 at 10:23

1 Answers1

6

I've just solved this for my own project. It does appear, as suggested in your 2nd question, that any inline formset validation requiring access to the parent form needs to be in the clean method of a BaseInlineFormset subclass.

Happily, the parent form's instance gets created (or retrieved from the database, if you're modifying rather than creating it) before the inline formset's clean is called, and it is available there as self.instance.

from django.core.exceptions import ValidationError
class InlineFormset(forms.models.BaseInlineFormSet):
    def clean(self):
        try:
            forms = [f for f in self.forms
                       if  f.cleaned_data
                       # This next line filters out inline objects that did exist
                       # but will be deleted if we let this form validate --
                       # obviously we don't want to count those if our goal is to
                       # enforce a min or max number of related objects.
                       and not f.cleaned_data.get('DELETE', False)]
            if self.instance.parent_foo == 'bar':
                if len(forms) == 0:
                    raise ValidationError(""" If the parent object's 'foo' is
                    'bar' then it needs at least one related object! """)
        except AttributeError:
            pass

class InlineAdmin(admin.TabularInline):
    model = ParentModel.inlineobjects.through
    formset = InlineFormset

The try-except pattern here is guarding against an AttributeError corner case that I haven't seen myself but which apparently arises when we try to access the cleaned_data attribute of a form (in self.forms) that failed to validate. Learned about this from https://stackoverflow.com/a/877920/492075

(NB: my project is still on Django 1.3; haven't tried this in 1.4)

Daniel Holmes
  • 1,952
  • 2
  • 17
  • 28
Christian Brink
  • 663
  • 4
  • 18
  • The `instance` member of the `InlineFormset` is still populated when the `clean()` method is called by the *admin* as of Django 1.8. Thank you ! – Ad N Aug 03 '15 at 07:53
  • 1
    I did something pretty similar in Django 1.9 and it worked just fine. The one thing I would add is adding a `super(InlineFormset, self).clean()` at the top of the overridden clean function gives you access to `cleaned_data` on each of the forms in the formset. – David Reynolds Aug 24 '16 at 10:49