21

I'm using a TabularInline in Django's admin, configured to show one extra blank form.

class MyChildInline(admin.TabularInline):
    model = MyChildModel
    form = MyChildInlineForm
    extra = 1

The model looks like MyParentModel->MyChildModel->MyInlineForm.

I'm using a custom form so I can dynamically lookup values and populate choices in a field. e.g.

class MyChildInlineForm(ModelForm):

    my_choice_field = forms.ChoiceField()

    def __init__(self, *args, **kwargs):
        super(MyChildInlineForm, self).__init__(*args, **kwargs)

        # Lookup ID of parent model.
        parent_id = None
        if "parent_id" in kwargs:
            parent_id = kwargs.pop("parent_id")
        elif self.instance.parent_id:
            parent_id = self.instance.parent_id
        elif self.is_bound:
            parent_id = self.data['%s-parent'% self.prefix]

        if parent_id:
            parent = MyParentModel.objects.get(id=parent_id)
            if rev:
                qs = parent.get_choices()
                self.fields['my_choice_field'].choices = [(r.name,r.value) for r in qs]

This works fine for the inline records bound to an actual record, but for the extra blank form, it doesn't display any values in my choice field, since it doesn't have any record id and there can't lookup the associated MyParentModel record.

I've inspected all the values I could find (args, kwargs, self.data, self.instance, etc) but I can't find any way to access the parent object the tabular inline is bound to. Is there any way to do this?

Cerin
  • 60,957
  • 96
  • 316
  • 522

4 Answers4

33

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.

Cerin
  • 60,957
  • 96
  • 316
  • 522
vdboor
  • 21,914
  • 12
  • 83
  • 96
  • +1, although I'm not sure what @Cerin was exactly trying to solve, most of the time it should be easier to use generic inlines and use the formset instance to check for contenttype and object_id. – Hedde van der Heide Dec 15 '12 at 10:17
  • 3
    I tried this, but I am getting a KeyError for 'parent_object' in the line of MyForm where you try to pop the parent_object value. – platzhersh Jun 22 '16 at 07:23
  • Works perfect! Make sure to pop before calling super, and skip for empty forms. – KinoP Mar 22 '17 at 16:21
  • @platzhersh: the pop error happens because of the `empty_form`. It doesn't get constructed through `_construct_form()`, so I added `.pop('parent_object', None)`. However, the Django 1.9+ solution works better in both cases! – vdboor May 04 '17 at 09:17
  • 1
    Tested it again in Django 1.11. Works perfectly. – Cerin Jun 23 '17 at 04:18
  • 1
    @vdboor, I hope you don't mind my edits. Went digging through the code and I figured out how to support `empty_form`. – Cerin Jul 11 '17 at 01:40
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
Ivan Semochkin
  • 8,649
  • 3
  • 43
  • 75
  • 1
    Nice to see the Django 1.9+ solution :-) Instead of `self.form_kwargs.copy()` I'd recommend calling `super()` instead. – vdboor May 04 '17 at 09:18
  • 2 questions: 1) what is FormFieldMetaForm? shouldn't it be super(MyForm,self)? 2) in MyForm.__init__(), when actually making use of parent, should it be done before or after the call to super().__init__()? – ckot Jun 22 '17 at 00:01
  • the answers to my above questions are 1) 'yes', and 2) 'after'. I'm editting this answer to reflect that – ckot Jun 22 '17 at 14:10
  • Works well with Django 1.11! – nspo Aug 02 '17 at 08:36
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
  • 5,718
  • 1
  • 20
  • 17
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
  • 10,021
  • 2
  • 37
  • 41