Skip to main content

Working with Inline Models

To manage related models on the same page as a parent model in the Django admin, you use Inlines. This allows you to edit objects with a ForeignKey or ManyToManyField relationship directly within the parent's change form.

Basic Implementation​

To add an inline, define a class inheriting from TabularInline or StackedInline and include it in the inlines attribute of your ModelAdmin.

from django.contrib import admin
from .models import Author, Book

# TabularInline displays related objects in a compact, table-like layout
class BookInline(admin.TabularInline):
model = Author.books.through

class AuthorAdmin(admin.ModelAdmin):
inlines = [
BookInline,
]

admin.site.register(Author, AuthorAdmin)

Layout Options​

  • TabularInline: Displays fields in a horizontal table. Best for models with few fields.
  • StackedInline: Displays fields in a vertical stack, similar to the main ModelAdmin. Best for models with many fields or complex layouts.

Customizing Inline Counts​

You can control how many empty forms are displayed and the limits on the number of related objects using extra, min_num, and max_num.

class PhotoInline(admin.StackedInline):
model = Photo
extra = 2 # Number of empty forms to display
max_num = 10 # Maximum number of related objects allowed
min_num = 1 # Minimum number of forms that must be filled

Dynamic Counts​

If you need to calculate these values based on the specific object being edited, override the get_extra, get_min_num, or get_max_num methods.

class BinaryTreeAdmin(admin.TabularInline):
model = BinaryTree

def get_extra(self, request, obj=None, **kwargs):
extra = 2
if obj:
# Reduce extra forms based on existing related objects
return extra - obj.binarytree_set.count()
return extra

def get_max_num(self, request, obj=None, **kwargs):
max_num = 3
if obj:
return max_num - obj.binarytree_set.count()
return max_num

Organizing Fields with Fieldsets​

Inlines support fieldsets just like ModelAdmin, allowing you to group fields or make them collapsible.

class PhotoInline(admin.StackedInline):
model = Photo
fieldsets = [
(None, {
"fields": ["image", "title"],
"description": "Primary Information"
}),
("Details", {
"fields": ["description", "creation_date"],
"classes": ["collapse"], # Makes this section collapsible
"description": "Additional Metadata",
}),
]

Advanced Configuration​

Handling Multiple Foreign Keys​

If the related model has more than one ForeignKey to the parent model, you must explicitly specify which one to use with fk_name.

class SightingInline(admin.TabularInline):
model = Sighting
fk_name = "location" # Specify which FK points to the parent model

Custom Forms and Validation​

You can use a custom ModelForm to add specific validation logic or custom widgets to your inlines.

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

class TitleForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
if cleaned_data.get("title1") != cleaned_data.get("title2"):
raise ValidationError("The two titles must be the same")
return cleaned_data

class TitleInline(admin.TabularInline):
model = Title
form = TitleForm
extra = 1

Controlling Permissions​

You can restrict actions within the inline by overriding permission methods.

class ReadOnlyChapterInline(admin.TabularInline):
model = Chapter

def has_add_permission(self, request, obj=None):
return False # Prevent adding new chapters from this inline

def has_change_permission(self, request, obj=None):
return False # Make existing chapters read-only

def has_delete_permission(self, request, obj=None):
return False # Prevent deletion

Troubleshooting​

Protected Deletions​

The InlineModelAdmin uses a specialized DeleteProtectedModelForm (found in django/contrib/admin/options.py) to prevent the deletion of related objects that are protected by database constraints. If you attempt to delete an inline object that has protected relationships, the admin will raise a ValidationError listing the objects that prevent the deletion.

Missing "Add Another" Button​

If the "Add another" button is missing, check:

  1. If max_num has been reached.
  2. If the user has add permissions for the related model.
  3. If has_add_permission has been overridden to return False.
  4. If the parent model is an auto-created ManyToMany "through" model; in this case, permissions are derived from the parent model's change permission.