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:
- TabularInline: Displays related objects in a compact, table-like format. This is ideal for models with few fields.
- StackedInline: Displays related objects in a vertical stack, similar to the main
ModelAdminform. 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
Linking to the Related Admin Page
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.