Inline Models

Difficulty: Intermediate

Inline models allow you to edit related objects on the same page as their parent object. Instead of navigating to a separate page to add or edit related items, you see them inline within the parent's edit form. This is especially useful for one-to-many relationships where the child objects are closely tied to the parent.

Django provides two inline classes: TabularInline displays related objects in a compact table format (one row per object), and StackedInline displays them in a stacked format (one block per object, similar to the standard edit form). TabularInline is better for objects with few fields, while StackedInline is better for objects with many fields.

To use inlines, create a class that inherits from admin.TabularInline or admin.StackedInline, set the 'model' attribute to the related model, and add the inline class to the parent ModelAdmin's 'inlines' list. The related model must have a ForeignKey pointing to the parent model.

The 'extra' attribute controls how many empty forms appear for adding new related objects (default is 3). Set extra=0 if you do not want empty forms by default. 'min_num' sets the minimum number of inline forms. 'max_num' limits the total number of inline forms. 'show_change_link' adds a link to the full change form for each inline object.

You can customize inline fields using the same attributes as ModelAdmin: fields, exclude, readonly_fields, and custom methods. You can also override get_queryset() to filter which related objects appear, and formfield_for_foreignkey() to customize dropdown fields.

Code examples

TabularInline Example

from django.contrib import admin
from .models import Order, OrderItem

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 1
    min_num = 1
    fields = ['product', 'quantity', 'unit_price', 'line_total']
    readonly_fields = ['line_total']

    @admin.display(description='Line Total')
    def line_total(self, obj):
        if obj.pk:
            return f'${obj.quantity * obj.unit_price:.2f}'
        return '-'

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'customer', 'status', 'total', 'created_at']
    inlines = [OrderItemInline]

TabularInline shows OrderItems as rows in a table within the Order edit page. extra=1 shows one empty row for adding items. readonly_fields and custom methods work like in ModelAdmin.

StackedInline Example

from django.contrib import admin
from .models import Article, Comment

class CommentInline(admin.StackedInline):
    model = Comment
    extra = 0  # No empty forms
    fields = ['author', 'content', 'is_approved', 'created_at']
    readonly_fields = ['created_at']
    show_change_link = True

    def get_queryset(self, request):
        """Show only recent comments."""
        qs = super().get_queryset(request)
        return qs.order_by('-created_at')[:20]

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'comment_count']
    inlines = [CommentInline]

    @admin.display(description='Comments')
    def comment_count(self, obj):
        return obj.comments.count()

StackedInline displays each comment as a block with full field layout. extra=0 means no empty forms appear. show_change_link adds a link to each comment's full edit page. get_queryset limits to recent comments.

Multiple Inlines

from django.contrib import admin
from .models import Product, ProductImage, ProductReview, ProductVariant

class ProductImageInline(admin.TabularInline):
    model = ProductImage
    extra = 1
    max_num = 10

class ProductVariantInline(admin.TabularInline):
    model = ProductVariant
    extra = 0
    fields = ['size', 'color', 'sku', 'stock']

class ProductReviewInline(admin.StackedInline):
    model = ProductReview
    extra = 0
    readonly_fields = ['author', 'rating', 'content', 'created_at']
    can_delete = False  # Prevent deleting reviews from product page

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'price', 'is_active']
    inlines = [ProductImageInline, ProductVariantInline, ProductReviewInline]

A single parent ModelAdmin can have multiple inlines. Here, the Product edit page shows images (tabular), variants (tabular), and reviews (stacked). can_delete=False prevents deletion of reviews from the inline.

GenericTabularInline for Generic Relations

from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from .models import Article, Product, Tag

class TagInline(GenericTabularInline):
    model = Tag
    extra = 1

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    inlines = [TagInline]

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    inlines = [TagInline]  # Same inline reused for different models

GenericTabularInline works with models using Django's contenttypes framework for generic foreign keys. The same Tag inline can be reused across Article and Product admin pages.

Key points

Concepts covered

TabularInline, StackedInline, inlines, extra, ForeignKey Inline