Skip to main content

Inline Model Editing

Inline model editing allows you to manage related objects directly on the parent object's admin page. Instead of navigating to a separate page to add or edit related items, you can configure them to appear as "inlines" within the parent's change form.

Basic Inline Configuration

To enable inline editing, define a class that inherits from TabularInline or StackedInline, specify the related model, and add that class to the inlines list of your ModelAdmin.

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

class BookInline(admin.TabularInline):
model = Book
extra = 3 # Number of empty forms to display

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

admin.site.register(Author, AuthorAdmin)

Tabular vs. Stacked Layouts

This project provides two primary layouts for inlines:

  1. TabularInline: Displays related objects in a compact, table-like format. This is ideal for models with few fields.
  2. StackedInline: Displays related objects in a vertical stack, similar to the main ModelAdmin form. This is better for models with many fields or complex layouts.
# Compact table layout
class PhotoTabularInline(admin.TabularInline):
model = Photo

# Full-width stacked layout
class PhotoStackedInline(admin.StackedInline):
model = Photo

Handling Multiple Foreign Keys

If the related model has more than one ForeignKey to the parent model, you must specify fk_name to tell the admin which relationship to use.

In tests/admin_views/models.py, the Article model has multiple foreign keys to Section:

class Article(models.Model):
title = models.CharField(max_length=100)
section = models.ForeignKey(Section, models.CASCADE)
another_section = models.ForeignKey(Section, models.CASCADE, related_name="+")

To create an inline for Article on the Section admin page, specify the target field:

class ArticleInline(admin.TabularInline):
model = Article
fk_name = "section" # Required because Article has multiple FKs to Section

Customizing Form Counts

You can control how many forms are displayed using extra, min_num, and max_num.

class PhotoInline(admin.TabularInline):
model = Photo
extra = 2 # Show 2 empty forms by default
min_num = 1 # Require at least 1 related object
max_num = 5 # Allow a maximum of 5 related objects

Dynamic Form Counts

For more complex logic, override get_extra or get_max_num. For example, you might want to limit the total number of forms based on existing objects:

class BinaryTreeAdmin(admin.TabularInline):
model = BinaryTree

def get_extra(self, request, obj=None, **kwargs):
extra = 2
if obj:
# Adjust extra forms based on current count
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

Advanced UI Options

Use show_change_link = True to provide a direct link to the full admin change form for each related object. This is useful if the inline only shows a subset of fields.

class LinkedInline(admin.TabularInline):
model = Photo
fields = ["title"]
show_change_link = True

Collapsible Inlines

You can make inlines collapsible by adding the "collapse" class. This is particularly useful for StackedInline layouts that take up a lot of vertical space.

class CollapsibleInline(admin.StackedInline):
model = Photo
classes = ["collapse"]

Permissions and Deletion

Inlines respect the user's permissions for the related model. You can further restrict these within the inline class:

class RestrictedInline(admin.TabularInline):
model = Photo
can_delete = False # Remove the 'Delete' checkbox from the inline

def has_add_permission(self, request, obj=None):
# Only allow adding if the parent object already exists
return obj is not None

Troubleshooting: Protected Objects

If a related object is protected (e.g., using on_delete=models.PROTECT), the admin will prevent deletion through the inline and raise a ValidationError during the cleaning process in InlineModelAdmin.get_formset.

# Internal validation logic in django/contrib/admin/options.py
def hand_clean_DELETE(self):
if self.cleaned_data.get(DELETION_FIELD_NAME, False):
# ... collection logic ...
if collector.protected:
raise ValidationError(
_("Deleting %(class_name)s %(instance)s would require deleting "
"the following protected related objects: %(related_objects)s"),
code="deleting_protected",
params=params
)

If you encounter this error, ensure that the related objects are not being referenced by other models with PROTECT constraints before attempting to delete them via the inline form.