[Django]-Accessing parent model instance from modelform of admin inline

34đź‘Ť

Update: As of Django 1.9, there is a def get_form_kwargs(self, index) method in the BaseFormSet class. Hence, overriding that passes the data to the form.

This would be the Python 3 / Django 1.9+ version:

class MyFormSet(BaseInlineFormSet):
    def get_form_kwargs(self, index):
        kwargs = super().get_form_kwargs(index)
        kwargs['parent_object'] = self.instance
        return kwargs


class MyForm(forms.ModelForm):
    def __init__(self, *args, parent_object, **kwargs):
        self.parent_object = parent_object
        super(MyForm, self).__init__(*args, **kwargs)


class MyChildInline(admin.TabularInline):
    formset = MyFormSet
    form = MyForm

For Django 1.8 and below:

To pass a value of a formset to the individual forms, you’d have to see how they are constructed. An editor/IDE with “jump to definition” really helps here to dive into the ModelAdmin code, and learn about the inlineformset_factory and it’s BaseInlineFormSet class.

From there you’ll find that the form is constructed in _construct_form() and you can override that to pass extra parameters. It will likely look something like this:

class MyFormSet(BaseInlineFormSet):
    def _construct_form(self, i, **kwargs):
        kwargs['parent_object'] = self.instance
        return super(MyFormSet, self)._construct_form(i, **kwargs)

    @property
    def empty_form(self):
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True,
            parent_object=self.instance,
        )
        self.add_fields(form, None)
        return form

class MyForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        self.parent_object = kwargs.pop('parent_object', None)
        super(MyForm, self).__init__(*args, **kwargs)


class MyChildInline(admin.TabularInline):
    formset = MyFormSet
    form = MyForm

Yes, this involves a private _construct_form function.

update Note: This doesn’t cover the empty_form, hence your form code needs to accept the parameters optionally.

👤vdboor

5đź‘Ť

I’m using Django 1.10 and it works for me:
Create a FormSet and put the parent object into kwargs:

class MyFormSet(BaseInlineFormSet):

    def get_form_kwargs(self, index):
        kwargs = super(MyFormSet, self).get_form_kwargs(index)
        kwargs.update({'parent': self.instance})
        return kwargs

Create a Form and pop an atribute before super called

class MyForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        parent = kwargs.pop('parent')
        super(MyForm, self).__init__(*args, **kwargs)
        # do whatever you need to with parent

Put that in the inline admin:

class MyModelInline(admin.StackedInline):
    model = MyModel
    fields = ('my_fields', )
    form = MyFrom
    formset = MyFormSet

2đź‘Ť

AdminModel has some methods like get_formsets. It receives an object and returns a bunch of formsets. I think you can add some info about parent object to that formset classes and use it later in formset’s __init__

👤ilvar

1đź‘Ť

Expanding on ilvar’s answer a bit, If the form field of interest is constructed from a model field, you can use the following construction to apply custom behavior to it:

class MyChildInline(admin.TabularInline):
    model = MyChildModel
    extra = 1
    def get_formset(self, request, parent=None, **kwargs):
        def formfield_callback(db_field):
            """
            Constructor of the formfield given the model field.
            """
            formfield = self.formfield_for_dbfield(db_field, request=request)
            if db_field.name == 'my_choice_field' and parent is not None:
                formfield.choices = parent.get_choices()
            return formfield
        return super(MyChildInline, self).get_formset(
            request, obj=obj, formfield_callback=formfield_callback, **kwargs)
        return result
👤Lucas Wiman

Leave a comment