Django-Admin.ru

Инлайны в Django Admin: TabularInline и StackedInline

Django admin inline на практике: TabularInline или StackedInline, extra и max_num, ManyToMany через through, GenericInline, валидация, права и N+1.

Редакция Django-Admin.ru
· 18 мин чтения

Содержание · 11 разделов
  1. Как подключить инлайн
  2. Направление связи и fk_name
  3. TabularInline или StackedInline
  4. Параметры инлайна
  5. extra, min_num и max_num
  6. fields и readonly_fields
  7. Свёрнутый инлайн: classes = [“collapse”]
  8. show_change_link и ordering
  9. Инлайны для ManyToMany
  10. Обычное ManyToManyField
  11. Промежуточная модель с данными (through)
  12. GenericTabularInline и GenericStackedInline
  13. Вложенные инлайны
  14. Производительность: N+1 и отсутствие пагинации
  15. Выпадающий список на каждой строке
  16. Пагинации нет: все строки и лимит в 1000 полей
  17. Валидация formset: clean в BaseInlineFormSet
  18. Действия после сохранения строк
  19. Права на инлайны
  20. Список выбора в зависимости от родителя
  21. Частые вопросы
  22. Почему инлайн не появляется на странице?
  23. Как сделать инлайн только для чтения?
  24. Как выполнить JavaScript при добавлении строки?
  25. Можно ли менять порядок строк перетаскиванием?
  26. Как добавить инлайн, если связь OneToOneField?
  27. Чем инлайн отличается от list_editable?
  28. Итог

Инлайн (inline) в Django Admin — это редактирование связанных объектов прямо на странице родителя: позиции заказа на странице заказа, фото товара на странице товара. Чтобы его включить, опишите класс-наследник admin.TabularInline (строки таблицы) или admin.StackedInline (блоки формы) для дочерней модели с ForeignKey на родителя и перечислите его в inlines у ModelAdmin родителя.

Ниже — выбор между двумя видами, все полезные параметры, инлайны для ManyToMany и generic-связей, вложенность, производительность, валидация и права. Примеры проверены на Django 6.1 через тестовый клиент; для 5.2 LTS они те же. Общая настройка админки описана в руководстве по Django Admin.

Как подключить инлайн

Возьмём заказы интернет-магазина:

# shop/models.py
from django.db import models


class Product(models.Model):
    name = models.CharField("Название", max_length=200)
    price = models.DecimalField("Цена", max_digits=10, decimal_places=2)
    is_active = models.BooleanField("В продаже", default=True)

    def __str__(self):
        return self.name


class Order(models.Model):
    number = models.CharField("Номер", max_length=20, unique=True)
    customer = models.CharField("Покупатель", max_length=200)
    is_paid = models.BooleanField("Оплачен", default=False)

    def __str__(self):
        return f"Заказ {self.number}"


class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
    product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар")
    quantity = models.PositiveIntegerField("Количество", default=1)
    price = models.DecimalField("Цена", max_digits=10, decimal_places=2)

    class Meta:
        verbose_name = "позиция заказа"
        verbose_name_plural = "позиции заказа"

    def __str__(self):
        return f"{self.product} × {self.quantity}"

Инлайн описывает дочернюю модель OrderItem, а подключается к админке родителя Order:

# shop/admin.py
from django.contrib import admin

from .models import Order, OrderItem, Product


class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ["number", "customer", "is_paid"]
    inlines = [OrderItemInline]

На странице заказа появится таблица «Позиции заказа»: по строке на каждую позицию и ссылка для добавления новой. Регистрировать OrderItem отдельно не нужно; как регистрировать модели и чем admin.site.register отличается от декоратора, разобрано в статье «Как зарегистрировать модель в Django Admin».

Направление связи и fk_name

Инлайн работает только «сверху вниз»: у модели инлайна должен быть ForeignKey на модель страницы. Если перепутать направление, например вставить инлайн заказов в админку товара, проверка при запуске остановит проект:

admin.E202 'shop.Order' has no ForeignKey to 'shop.Product'.

Когда дочерняя модель ссылается на родителя двумя ключами (сопутствующие товары: product и related — оба на Product), Django не знает, какой из них использовать:

admin.E202 'shop.RelatedProduct' has more than one ForeignKey to 'shop.Product'. You must specify a 'fk_name' attribute.

Укажите ключ явно:

class RelatedProductInline(admin.TabularInline):
    model = RelatedProduct
    fk_name = "product"

Подробнее о внешних ключах, related_name и on_delete — в статье «ForeignKey в Django».

TabularInline или StackedInline

Оба класса — подклассы InlineModelAdmin, у них одинаковые параметры, а отличаются они только шаблоном. Поэтому переключиться можно в любой момент, поменяв базовый класс.

КритерийTabularInlineStackedInline
ВидСтрока таблицы на объект, поля — колонкиОтдельный блок формы на объект, поля друг под другом
Сколько полей удобно2–6 короткихЛюбое количество
TextField, изображения, JSONРастягивают таблицу по ширинеОтображаются нормально
fieldsets с заголовкамиЗаголовки групп не выводятся, поля идут подрядГруппы и заголовки сохраняются
Много связанных объектовКомпактно, видно сразу 20–30 строкСтраница быстро становится длинной
Типичные примерыПозиции заказа, цены, контактыАдреса, разделы статьи, варианты с описанием

Правило выбора простое: короткие однородные строки — TabularInline, развёрнутые объекты с длинными полями — StackedInline. Если нужны и компактный список, и полная форма, оставьте TabularInline с ключевыми полями и включите show_change_link: из строки откроется полная страница объекта.

Параметры инлайна

Инлайн поддерживает большинство опций формы ModelAdmin: fields, fieldsets, exclude, readonly_fields, ordering, autocomplete_fields, raw_id_fields, radio_fields, prepopulated_fields, formfield_for_foreignkey. Собственные параметры инлайна:

ПараметрПо умолчаниюЧто делает
model—Дочерняя модель, обязательный параметр
extra3Сколько пустых форм показать для новых объектов
min_numNoneМинимум форм на странице
max_numNone (1000 форм)Максимум форм; при достижении кнопка «Добавить» скрывается
can_deleteTrueКолонка «Удалить?» у существующих строк
show_change_linkFalseСсылка «Изменить» на страницу дочернего объекта
classesNoneCSS-классы блока; ["collapse"] сворачивает инлайн
verbose_name, verbose_name_pluralиз Meta моделиЗаголовок блока и текст кнопки добавления
fk_nameNoneКакой ForeignKey использовать, если их несколько
formset, formBaseInlineFormSetСвой набор форм или форма строки — для валидации
templateпо классуСвой шаблон инлайна

Типичная рабочая настройка:

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0  # без пустых строк: добавляют кнопкой
    min_num = 1
    fields = ["product", "quantity", "price"]
    autocomplete_fields = ["product"]
    ordering = ["-quantity"]
    show_change_link = True

extra, min_num и max_num

extra = 3 по умолчанию добавляет три пустые строки, которые только мешают — почти всегда ставьте extra = 0. Ссылка «Добавить еще…» работает на JavaScript и добавляет строки без перезагрузки страницы.

min_num и max_num управляют интерфейсом: сколько форм отрисовать, когда скрыть кнопку добавления и кнопки удаления. Первые min_num пустых форм становятся обязательными, поэтому пустая строка вернётся с ошибкой «Обязательное поле.».

Важно: сервер эти границы не проверяет. Админка создаёт formset без validate_min и validate_max, поэтому запрос с нулём строк при min_num = 1 или с пятью строками при max_num = 2 сохранится. Мы проверили оба случая тестовым клиентом на Django 6.1: заказ записался без ошибок.

Если ограничение — бизнес-правило, которое нельзя обойти, включите серверную проверку в get_formset:

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    min_num = 1
    max_num = 20

    def get_formset(self, request, obj=None, **kwargs):
        kwargs["validate_min"] = True
        kwargs["validate_max"] = True
        return super().get_formset(request, obj, **kwargs)

Теперь нарушение возвращает ошибку над инлайном: «Пожалуйста, отправьте не больше 20 форм.» или «Пожалуйста, отправьте 1 форму.». Сложные правила вроде «сумма заказа не меньше 500 ₽» пишут в clean() формсета — о нём ниже.

Число форм можно вычислять: методы get_extra(), get_min_num() и get_max_num() получают request и родительский объект obj (на странице добавления — None). Для связи OneToOneField Django сам ставит max_num = 1.

fields и readonly_fields

fields задаёт колонки и их порядок, exclude убирает поля. Поле из readonly_fields выводится только если оно перечислено в fields (когда fields задан): иначе оно молча пропадает. В readonly_fields можно передавать и методы инлайна — так выводят вычисляемые значения:

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    fields = ["product", "quantity", "price", "line_total"]
    readonly_fields = ["line_total"]

    @admin.display(description="Сумма")
    def line_total(self, obj):
        if obj.pk is None:
            return "—"
        return obj.quantity * obj.price

Свёрнутый инлайн: classes = [“collapse”]

Пример — блок заметок к заказу (модель Note с generic-связью разобрана ниже):

from django.contrib.contenttypes.admin import GenericStackedInline


class NoteInline(GenericStackedInline):
    model = Note
    extra = 0
    classes = ["collapse"]

С Django 5.1 свёрнутый блок выводится HTML-элементами <details> и <summary>: заголовок инлайна становится кликабельным, а отдельный скрипт collapse.js больше не нужен. Сворачивать стоит второстепенные блоки — заметки, историю, служебные данные.

show_change_link = True добавляет у каждой сохранённой строки ссылку «Изменить» на страницу объекта. Ссылка появляется, только если дочерняя модель зарегистрирована в админке: без admin.site.register(OrderItem) её не будет.

ordering сортирует строки инлайна независимо от Meta.ordering модели. Для ручного порядка добавьте в модель поле position и сортируйте по нему.

Инлайны для ManyToMany

У связи «многие ко многим» нет ForeignKey на родителя, но есть промежуточная таблица с двумя ключами. Инлайн строится на ней.

Обычное ManyToManyField

class Tag(models.Model):
    name = models.CharField("Название", max_length=50)


class Product(models.Model):
    # ...
    tags = models.ManyToManyField(Tag, blank=True, related_name="products")

На странице товара теги и так выбираются виджетом множественного выбора. Инлайн нужен на обратной стороне — чтобы со страницы тега видеть и добавлять товары:

class ProductTagInline(admin.TabularInline):
    model = Product.tags.through  # промежуточная модель, созданная Django
    extra = 1
    autocomplete_fields = ["product"]
    verbose_name = "товар с тегом"
    verbose_name_plural = "товары с тегом"


@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    search_fields = ["name"]
    inlines = [ProductTagInline]

Если такой инлайн ставите на стороне модели с полем (ProductAdmin), исключите само поле через exclude = ["tags"], иначе на странице будет два способа редактировать одну связь.

Две особенности автоматической промежуточной модели:

  • своих прав у неё нет. Инлайн доступен пользователю с правом change на модель на другом конце связи: на странице тега без права «Может изменять товар» блок не отображается;
  • сигнал m2m_changed не срабатывает: для админки это обычная модель с двумя ключами. Логику, привязанную к этому сигналу, перенесите в save_related() или сигналы промежуточной модели.

Промежуточная модель с данными (through)

Когда у связи есть свои поля — закупочная цена, срок поставки, — описывают модель through. Для такого ManyToManyField админка вообще не выводит виджет, и инлайн остаётся единственным способом редактировать связь из интерфейса:

class Supplier(models.Model):
    name = models.CharField("Название", max_length=200)


class Product(models.Model):
    # ...
    suppliers = models.ManyToManyField(Supplier, through="Supply", blank=True)


class Supply(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
    purchase_price = models.DecimalField("Закупочная цена", max_digits=10, decimal_places=2)
    lead_days = models.PositiveSmallIntegerField("Срок поставки, дней", default=7)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=["product", "supplier"], name="unique_supply")
        ]
class SupplyInline(admin.TabularInline):
    model = Supply
    extra = 1
    autocomplete_fields = ["supplier"]


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    search_fields = ["name"]
    inlines = [SupplyInline]


@admin.register(Supplier)
class SupplierAdmin(admin.ModelAdmin):
    search_fields = ["name"]
    inlines = [SupplyInline]

Один и тот же инлайн работает с обеих сторон: Django сам находит ключ на модель текущей страницы. UniqueConstraint защищает от дублей: при попытке добавить поставщика дважды админка покажет ошибку валидации «Поставка с такими значениями полей Product и Supplier уже существует.» вместо ошибки базы данных.

GenericTabularInline и GenericStackedInline

Для моделей с GenericForeignKey — заметок, файлов, комментариев, которые цепляются к любым объектам, — используют классы из django.contrib.contenttypes.admin:

# shop/models.py
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType


class Note(models.Model):
    text = models.TextField("Текст")
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveBigIntegerField()
    content_object = GenericForeignKey("content_type", "object_id")
# shop/admin.py
from django.contrib.contenttypes.admin import GenericTabularInline


class NoteInline(GenericTabularInline):
    model = Note
    extra = 0


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    inlines = [OrderItemInline, NoteInline]

Поля content_type и object_id в форме не показываются: при сохранении Django заполняет их сам. Если поля в модели называются иначе, укажите ct_field и ct_fk_field. Тот же NoteInline можно подключить к товарам, поставщикам и любой другой модели.

Вложенные инлайны

Встроенного вложения нет. Атрибут inlines внутри инлайна Django молча игнорирует: проверки проходят, но третий уровень (товар → вариант → фото варианта) на странице не появляется.

Встроенный обходной путь — два уровня с переходом: на странице товара инлайн вариантов с show_change_link = True, а у VariantAdmin свой инлайн фотографий. Пользователь переходит по ссылке, но всё работает без сторонних пакетов.

Если редактировать всё на одной странице действительно нужно, подключите django-nested-admin:

pip install django-nested-admin
# settings.py
INSTALLED_APPS = [
    # ...
    "nested_admin",
]
# shop/admin.py
import nested_admin


class VariantImageInline(nested_admin.NestedTabularInline):
    model = VariantImage
    extra = 0


class VariantInline(nested_admin.NestedStackedInline):
    model = Variant
    extra = 0
    inlines = [VariantImageInline]


@admin.register(Product)
class ProductAdmin(nested_admin.NestedModelAdmin):
    inlines = [VariantInline]

Родительская админка и все инлайны на странице должны быть классами пакета: NestedModelAdmin, NestedStackedInline, NestedTabularInline, для generic-связей — NestedGenericTabularInline. Пакет также умеет сортировать строки перетаскиванием через sortable_field_name.

Важно: версия 4.1.6 официально тестируется до Django 5.2. На Django 6.1.2 мы проверили серверную часть тестовым клиентом: страница с тремя уровнями открывается, новая фотография варианта сохраняется. Работу перетаскивания и кнопок в браузере проверьте на своём проекте до обновления Django.

Производительность: N+1 и отсутствие пагинации

Выпадающий список на каждой строке

Поле ForeignKey в инлайне по умолчанию рендерится как <select> со всеми записями связанной таблицы, и запрос выполняется для каждой строки отдельно. Мы замерили страницу заказа с 20 позициями при 50 товарах в базе:

Настройка поля productЗапросов к таблице товаровЧто в HTML
По умолчанию (select)21 — вся таблица на каждую форму1071 элемент <option>
autocomplete_fields + select_related20 коротких запросов по idТолько выбранные значения
raw_id_fields + select_related20 коротких запросов по idПоле с ID и кнопка поиска
autocomplete_fields без select_related40 запросов по idТолько выбранные значения

Двадцать первая форма — шаблон для кнопки «Добавить». Оставшиеся 20 запросов у autocomplete_fields и raw_id_fields — подписи выбранных значений: каждый достаёт одну строку по первичному ключу и почти ничего не стоит. При 10 000 товаров вариант по умолчанию превращается в мегабайты HTML и секунды ожидания. Отсюда две настройки:

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    search_fields = ["name"]  # обязательно для autocomplete_fields


class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    autocomplete_fields = ["product"]

    def get_queryset(self, request):
        # __str__ позиции обращается к product — подгружаем одним JOIN
        return super().get_queryset(request).select_related("product")

select_related нужен из-за __str__: табличный инлайн выводит строковое представление каждого сохранённого объекта, а f"{self.product} × {self.quantity}" делает запрос на товар. raw_id_fields работает без search_fields, но редакторам неудобнее: выбор идёт через всплывающее окно со списком. Общий разбор проблемы и Debug Toolbar для подсчёта запросов — в статье «N+1 в Django ORM».

Пагинации нет: все строки и лимит в 1000 полей

Инлайн выводит все связанные объекты сразу. Кроме тяжёлой страницы, есть ограничение на сохранение: каждая строка отправляет свои поля плюс скрытые id и ссылку на родителя, а Django по умолчанию принимает не больше 1000 параметров (DATA_UPLOAD_MAX_NUMBER_FIELDS). Заказ с 200 позициями по три редактируемых поля уже не сохраняется: сервер отвечает 400 Bad Request с исключением TooManyFieldsSent в логе.

Варианты, если дочерних записей сотни:

  1. Показывать в инлайне последние N записей только для чтения, а полный список открывать ссылкой.
  2. Перенести редактирование в отдельный список дочерней модели с фильтром по родителю.
  3. Поднять DATA_UPLOAD_MAX_NUMBER_FIELDS до реального максимума, например 5000. Значение None снимает защиту от перегрузки запросами, его не ставьте.

Срез в get_queryset инлайна напрямую не работает: Django дальше фильтрует выборку по родителю и падает с TypeError: Cannot filter a query once a slice has been taken.. Возьмите ID отдельным запросом:

from django.urls import reverse
from django.utils.html import format_html


class LastItemsInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    can_delete = False
    fields = ["product", "quantity", "price"]
    readonly_fields = fields
    show_change_link = True
    verbose_name_plural = "последние 5 позиций"

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        last_ids = list(qs.order_by("-id").values_list("id", flat=True)[:5])
        return qs.filter(id__in=last_ids).select_related("product")

    def has_add_permission(self, request, obj):
        return False


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    inlines = [LastItemsInline]
    readonly_fields = ["items_link"]

    @admin.display(description="Все позиции")
    def items_link(self, obj):
        url = reverse("admin:shop_orderitem_changelist") + f"?order__id__exact={obj.pk}"
        return format_html('<a href="{}">Открыть все ({})</a>', url, obj.items.count())

Ссылка ведёт на список OrderItem, отфильтрованный по заказу; для этого OrderItem должна быть зарегистрирована в админке.

Валидация formset: clean в BaseInlineFormSet

Каждая строка инлайна — форма, а весь инлайн — набор форм (formset). Ошибки отдельного поля ловит форма строки, правила для всех строк сразу — метод clean() формсета:

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


class OrderItemFormSet(BaseInlineFormSet):
    def clean(self):
        super().clean()
        if any(self.errors):
            return  # сначала пусть пользователь исправит ошибки в строках
        products = set()
        for form in self.forms:
            if not form.cleaned_data or form.cleaned_data.get("DELETE"):
                continue
            product = form.cleaned_data["product"]
            if product in products:
                raise ValidationError(f"Товар «{product}» добавлен дважды.")
            products.add(product)
        if not products:
            raise ValidationError("В заказе должна быть хотя бы одна позиция.")


class OrderItemInline(admin.TabularInline):
    model = OrderItem
    formset = OrderItemFormSet
    extra = 0

Что важно в этом коде:

  • проверка any(self.errors) обязательна: у формы с ошибкой в cleaned_data нет части полей, и обращение к form.cleaned_data["product"] упадёт с KeyError;
  • пустые дополнительные формы дают пустой cleaned_data, а строки с отметкой «Удалить?» содержат DELETE = True — их пропускаем;
  • ошибка из clean() выводится над таблицей инлайна, а заказ не сохраняется целиком: родитель и строки записываются только когда валидны все формы.

self.instance внутри формсета — родительский объект. На странице добавления он ещё не сохранён, поэтому не стройте проверки на его pk и связанных выборках. Как устроены формы и ValidationError, рассказано в статье «Формы в Django».

Действия после сохранения строк

Частая ошибка — пересчитывать итог заказа в save_model(): в этот момент строки инлайна ещё не записаны. Родитель сохраняется первым, инлайны — в save_related():

from django.db.models import F, Sum


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    inlines = [OrderItemInline]

    def save_related(self, request, form, formsets, change):
        super().save_related(request, form, formsets, change)
        order = form.instance
        total = order.items.aggregate(s=Sum(F("quantity") * F("price")))["s"]
        # total уже учитывает новые и удалённые строки;
        # в save_model() для нового заказа здесь был бы None

Чтобы менять каждый объект перед записью (например, проставить автора), переопределите save_formset(): вызовите formset.save(commit=False), обработайте объекты, удалите formset.deleted_objects и вызовите formset.save_m2m().

Права на инлайны

Видимость и редактируемость инлайна определяют права на дочернюю модель:

  • без прав view_orderitem и change_orderitem блок позиций не показывается вовсе, даже если пользователь может редактировать заказ;
  • с одним правом просмотра строки выводятся только для чтения;
  • отдельные права add_orderitem и delete_orderitem включают кнопку добавления и колонку «Удалить?».

Как выдавать права пользователям и группам, разобрано в статье «Права доступа в Django».

Для правил, зависящих от родителя, переопределите методы has_*_permission. Они получают obj — родительский объект (на странице добавления None). Пример: оплаченный заказ нельзя менять:

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0

    def has_add_permission(self, request, obj):
        if obj is not None and obj.is_paid:
            return False
        return super().has_add_permission(request, obj)

    def has_change_permission(self, request, obj=None):
        if obj is not None and obj.is_paid:
            return False
        return super().has_change_permission(request, obj)

    def has_delete_permission(self, request, obj=None):
        if obj is not None and obj.is_paid:
            return False
        return super().has_delete_permission(request, obj)

У оплаченного заказа позиции выводятся текстом, без кнопки добавления и колонки удаления. Подделать запрос не выйдет: при запрете админка игнорирует изменения таких строк при сохранении.

Чтобы показать или скрыть инлайн целиком, переопределите get_inlines() у ModelAdmin:

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    def get_inlines(self, request, obj):
        if obj is None:
            return [OrderItemInline]  # при создании — только позиции
        return [OrderItemInline, NoteInline]

Список выбора в зависимости от родителя

Инлайн не получает родителя в formfield_for_foreignkey(), но ID есть в URL страницы изменения. Пример: снятые с продажи товары не предлагать, кроме тех, что уже есть в этом заказе:

from django.db.models import Q


class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 1

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "product":
            qs = Product.objects.filter(is_active=True)
            object_id = request.resolver_match.kwargs.get("object_id")
            if object_id:
                qs = Product.objects.filter(
                    Q(is_active=True) | Q(orderitem__order_id=object_id)
                ).distinct()
            kwargs["queryset"] = qs
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

Уже выбранные значения обязательно оставляйте в выборке: если товара существующей строки нет в queryset, при сохранении она получит ошибку «Выберите корректный вариант. Вашего варианта нет среди допустимых значений.».

Частые вопросы

Почему инлайн не появляется на странице?

Проверьте по порядку: класс указан в inlines той админки, страницу которой вы открыли; у модели инлайна есть ForeignKey на эту модель (иначе проект не запустится с ошибкой admin.E202); у пользователя есть хотя бы право просмотра дочерней модели; get_inlines() или has_view_permission() не возвращают пустой результат.

Как сделать инлайн только для чтения?

Верните False из has_add_permission(), has_change_permission() и has_delete_permission() инлайна. Вариант без методов: перечислите все поля в readonly_fields, поставьте can_delete = False и max_num = 0.

Как выполнить JavaScript при добавлении строки?

Админка генерирует событие formset:added на новой строке, а при удалении несохранённой строки — formset:removed на document. Подключите скрипт через class Media у ModelAdmin:

document.addEventListener('formset:added', (event) => {
  if (event.detail.formsetName === 'items') {
    const row = event.target;
    row.querySelector("input[name$='-quantity']").value = 1;
  }
});

Префикс формсета по умолчанию равен related_name ключа (items) или имени модели в нижнем регистре с суффиксом _set.

Можно ли менять порядок строк перетаскиванием?

Встроенный инлайн умеет только сортировать по ordering. Перетаскивание дают сторонние пакеты; например, django-nested-admin включает его параметром sortable_field_name с именем числового поля позиции.

Как добавить инлайн, если связь OneToOneField?

Так же, как для ForeignKey: Django увидит уникальный ключ и сам ограничит инлайн одной формой. Для профиля пользователя это стандартный способ показать поля профиля на странице пользователя.

Чем инлайн отличается от list_editable?

list_editable правит поля самой модели прямо в списке записей, а инлайн правит связанные объекты на странице одной записи. Для массовой правки цен у сотни товаров подходит первое, для состава конкретного заказа — второе.

Итог

  • Инлайн — класс TabularInline или StackedInline для дочерней модели с ForeignKey, подключённый в inlines родительской админки; при двух ключах на родителя нужен fk_name.
  • TabularInline — для коротких однородных строк, StackedInline — для объектов с длинными полями и группами fieldsets.
  • Ставьте extra = 0; min_num и max_num проверяются на сервере, только если включить validate_min и validate_max.
  • ManyToMany редактируют инлайном промежуточной модели (Model.field.through или своей through), generic-связи — GenericTabularInline.
  • Вложенных инлайнов нет: используйте show_change_link или django-nested-admin.
  • Для внешних ключей в строках включайте autocomplete_fields или raw_id_fields и select_related; для сотен строк показывайте последние записи и ссылку на список.
  • Правила для всех строк пишите в clean() формсета, действия после сохранения — в save_related(), ограничения по родителю — в has_*_permission(request, obj).

Полный список опций — в документации Django по InlineModelAdmin.

  • #Django Admin
  • #Инлайны
  • #TabularInline
  • #StackedInline
  • #Formset
↑ ↓ выборEnter открытьEsc закрыть