Инлайны в Django Admin: TabularInline и StackedInline
Django admin inline на практике: TabularInline или StackedInline, extra и max_num, ManyToMany через through, GenericInline, валидация, права и N+1.
Содержание · 11 разделов
- Как подключить инлайн
- Направление связи и fk_name
- TabularInline или StackedInline
- Параметры инлайна
- extra, min_num и max_num
- fields и readonly_fields
- Свёрнутый инлайн: classes = [“collapse”]
- show_change_link и ordering
- Инлайны для ManyToMany
- Обычное ManyToManyField
- Промежуточная модель с данными (through)
- GenericTabularInline и GenericStackedInline
- Вложенные инлайны
- Производительность: N+1 и отсутствие пагинации
- Выпадающий список на каждой строке
- Пагинации нет: все строки и лимит в 1000 полей
- Валидация formset: clean в BaseInlineFormSet
- Действия после сохранения строк
- Права на инлайны
- Список выбора в зависимости от родителя
- Частые вопросы
- Почему инлайн не появляется на странице?
- Как сделать инлайн только для чтения?
- Как выполнить JavaScript при добавлении строки?
- Можно ли менять порядок строк перетаскиванием?
- Как добавить инлайн, если связь OneToOneField?
- Чем инлайн отличается от list_editable?
- Итог
Инлайн (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, у них одинаковые параметры, а отличаются они только шаблоном. Поэтому переключиться можно в любой момент, поменяв базовый класс.
| Критерий | TabularInline | StackedInline |
|---|---|---|
| Вид | Строка таблицы на объект, поля — колонки | Отдельный блок формы на объект, поля друг под другом |
| Сколько полей удобно | 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 | — | Дочерняя модель, обязательный параметр |
extra | 3 | Сколько пустых форм показать для новых объектов |
min_num | None | Минимум форм на странице |
max_num | None (1000 форм) | Максимум форм; при достижении кнопка «Добавить» скрывается |
can_delete | True | Колонка «Удалить?» у существующих строк |
show_change_link | False | Ссылка «Изменить» на страницу дочернего объекта |
classes | None | CSS-классы блока; ["collapse"] сворачивает инлайн |
verbose_name, verbose_name_plural | из Meta модели | Заголовок блока и текст кнопки добавления |
fk_name | None | Какой ForeignKey использовать, если их несколько |
formset, form | BaseInlineFormSet | Свой набор форм или форма строки — для валидации |
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 и ordering
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_related | 20 коротких запросов по id | Только выбранные значения |
raw_id_fields + select_related | 20 коротких запросов по id | Поле с ID и кнопка поиска |
autocomplete_fields без select_related | 40 запросов по 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 в логе.
Варианты, если дочерних записей сотни:
- Показывать в инлайне последние N записей только для чтения, а полный список открывать ссылкой.
- Перенести редактирование в отдельный список дочерней модели с фильтром по родителю.
- Поднять
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