How to Pre-Populate a Django Inline Form for New Instances of the Base Object
Recently updated on
The client wanted the base model -- let’s call it Foo
-- to have a one-to-many relation with a secondary model -- let’s call it Bar
-- configured via an inline form in the Django admin. So far, so simple, right? You just set it up like this:
foos/models.py
from django.db import models class Foo(models.Model): name = models.CharField(max_length=255)
bars/models.py
from django.db import models from foos.models import Foo class Bar(models.Model): name = models.CharField(max_length=255) foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
foos/admin.py
from django.contrib import admin from bars.models import Bar from .models import Foo class BarInline(admin.StackedInline): model = Bar extra = 1 class FooAdmin(admin.ModelAdmin): inlines = [BarInline, ] admin.site.register(Foo, FooAdmin)
And the view to add a new Foo
instance looks like this:
But here’s where it got tricky. When a new Foo
is being added, the client wanted the first inline Bar
instance to appear with a default name
value, like this:
We might first try to do this by setting an initial value for the name
field by overriding the __init__()
method in a custom form. For example:
foos/admin.py
from django import forms from django.contrib import admin from bars.models import Bar from .models import Foo class BarInlineForm(forms.ModelForm): def __init__(self, *args, **kwargs): initial = kwargs.get("initial", {}) initial["name"] = "DEFAULT" kwargs["initial"] = initial super().__init__(*args, **kwargs) class BarInline(admin.StackedInline): model = Bar extra = 1 form = BarInlineForm class FooAdmin(admin.ModelAdmin): inlines = [BarInline, ] admin.site.register(Foo, FooAdmin)
The result looks promising, as the form is rendered with the desired value in place. But there are a couple of problems.
First, we only want this value to appear for the first inline on a new Foo
instance, but now the DEFAULT
value appears every time we add a new inline, regardless of whether the object is new or already has an ID.
Second, and more importantly, the if the user saves this new Foo
instance, the inline Bar
object will *not* be created. This happens because Django won’t create or save the related object if it detects that no changes have been made to the inline form. It makes this determination by comparing each field’s initial value with its value as submitted in the POST request. In our case, the form for the related Bar
instance was initialized with DEFAULT
in the name
field. Since we made no other changes to it before saving, the Bar
object was not created.
Let’s tackle the first of these two problems first. How can we know if we are dealing with the first inline on the Foo
instance and not the second, third, etc.? And how can we know if this is a new Foo
instance or an already existing one?
Since we’re already using a custom form, figuring out whether this is the first, second, (third, etc.) inline is pretty easy. You can get that information from the prefix
keyword argument that gets passed to the form’s __init__()
method. It’s trickier to know whether the parent Foo
is a new instance or an existing one because the inline Bar
form has no knowledge of its parent. So our first challenge will be to pass some reference to the parent into the inline form. Let’s consider that now:
One place where Django offers us a handle on the parent object is in the inline admin’s get_formset()
method. The parent object will be passed in with the obj
keyword. The method then gets a base formset class (by default, django.forms.models.BaseInlineFormset
) and returns a subclass of that class tailored to the specific admin form in question.
If we could override the get_form_kwargs()
method on that formset class, we could pass the parent object as a keyword argument to the child form. Here’s how we’ll do it. Let’s redefine our BarInline
class above like this:
class BarInline(admin.StackedInline): model = Bar extra = 1 form = BarInlineForm def get_formset(self, request, obj=None, **kwargs): # First get the base formset class BaseFormSet = kwargs.pop("formset", self.formset) # Now make a custom subclass with an overridden “get_form_kwargs()” class CustomFormSet(BaseFormSet): def get_form_kwargs(self, index): kwargs = super().get_form_kwargs(index) kwargs["parent_obj"] = obj return kwargs # Finally, pass our custom subclass to the superclass’s method. This # will override the default. kwargs["formset"] = CustomFormSet return super().get_formset(request, obj, **kwargs)
Those are all the changes we need to make to the custom admin class. But now we need to override the __init__()
method of our custom form class to handle several tasks.
The method needs to remove our new parent_obj
keyword argument from the kwargs
variable before passing it to the super()
call.
It needs to check the prefix
keyword argument to see if this is the first inline Bar
instance.
If this is the first inline Bar
and parent_obj
is `None` (meaning this is a new Foo
instance), then we need to take special action.
Our BarInlineForm
class will now look like this:
class BarInlineForm(forms.ModelForm): def __init__(self, *args, **kwargs): # Pop the ‘parent_obj’ keyword parent_obj = kwargs.pop("parent_obj") # Check if this is the first “bar” on a new “Foo” instance. if kwargs.get("prefix", "").endswith("-0") and parent_obj is None: # do stuff super().__init__(*args, **kwargs)
The last step is to replace the “do stuff” comment with real code. This addresses the second of the two problems above, and ensures that a new Bar
will be created on a new Foo
instance even when no other changes are made. Here’s the full form class:
class BarInlineForm(forms.ModelForm): def __init__(self, *args, **kwargs): # Pop the ‘parent_obj’ keyword parent_obj = kwargs.pop("parent_obj") # Check if this is the first “bar” on a new “Foo” instance. if kwargs.get("prefix", "").endswith("-0") and parent_obj is None: self.base_fields["name"] = type( "HackedCharField", (forms.CharField,), {"has_changed": lambda self, initial, data: super(forms.CharField, self).has_changed("", data) } )(initial="DEFAULT") super().__init__(*args, **kwargs)
Here, we’re using Python’s type
function to create a subclass of forms.CharField
called “HackedCharField” (though the exact name doesn’t matter). We assign this class a custom implementation of the has_changed()
method using a lambda function. The regular has_changed()
method accepts the initial and current field values as arguments to determine whether the field has changed since initialization. Our version accepts these same values (as it must), but then calls the superclass’s implementation using the empty string instead of the field’s true initialization value. Thus the field will recognize that same initialization value (the string DEFAULT
) as a *changed* value. As such, it will trigger creation of the related Bar
object.
Note that we must use the “old-school” Python syntax of passing the base class and self
to the super()
call. The use of super()
without arguments that has been available since Python 3 has problems when used in classes created with the type()
function. See bugs.python.org/issue29944 for more details.
And that’s it! If you liked this blog post or have any questions, please let us know!
Happy coding!
Need Upgrade Assistance?
Django 3.2 reaches its end of life in April 2024. If your site is on this or an earlier version, please fill out this form and we will reach out and provide a cost and time estimate for your consideration. No obligation.