skip to content

In Django's admin, how do you edit a course's lessons on the course's own change form, and when do you choose TabularInline over StackedInline?

level: middleimportance: must knowfreq 55%

answer

  1. child rows on the parent's page
  2. a class listed on the parent's ModelAdmin
  3. the only difference is the template
  4. extra defaults to three
  5. two foreign keys need a hint

basics

~20 s

Define a TabularInline or StackedInline subclass with model = Lesson and list it in the course ModelAdmin's inlines. Tabular renders one compact row per lesson, stacked renders each lesson as a full form; the only difference is the template.

solid answer

~40 s

An **inline** edits child rows that have a `ForeignKey` to the parent on the parent's change form. You subclass `admin.TabularInline` or `admin.StackedInline`, set `model = Lesson`, and add the class to `CourseAdmin.inlines`. Django finds the foreign key to `Course` automatically; if `Lesson` has two foreign keys to `Course`, you must set `fk_name`. The two classes differ only in template: tabular gives one table row per lesson, good for a few short fields; stacked lays each lesson out like a full form, better for long text or many fields. Useful options: `extra` (blank forms, default 3), `min_num`, `max_num`, `can_delete` (default `True`), `show_change_link`, and the usual `fields`, `readonly_fields`, `ordering` and `autocomplete_fields`, since inlines share most `ModelAdmin` options.

code

python · 17 lines
python
from django.contrib import admin

from courses.models import Course, Lesson


class LessonInline(admin.TabularInline):
    model = Lesson
    fk_name = "course"  # Lesson also has prerequisite_course -> Course
    fields = ["position", "title", "duration_minutes"]
    ordering = ["position"]
    extra = 1
    show_change_link = True


@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
    inlines = [LessonInline]

go deeper

for a junior

Know how to declare a TabularInline or StackedInline, list it in inlines, and that the two differ only in layout.

for a middle

Explain fk_name, extra, min_num, max_num, can_delete and show_change_link, and which ModelAdmin options inlines share.

for a senior

Show you cap inline sizes, avoid nested editing, use autocomplete or raw id widgets per row, and place post-save logic after inlines are saved.

for a principal

Judge when editing children inline stops scaling and the admin needs a separate workflow or a purpose-built internal tool.

## What an inline is The admin's change form edits one object. An **inline** adds a block to that page for editing related rows, the ones that point at this object with a `ForeignKey`. For an online-learning app, the course page can list, edit, add and delete its lessons without leaving the page. Under the hood each inline is an inline formset bound to the parent; the formset mechanics themselves are a forms topic. ## Declaring one 1. Subclass `admin.TabularInline` or `admin.StackedInline` and set `model` to the child model. 2. List the class in the parent `ModelAdmin`'s `inlines`. 3. Optionally override `get_inlines(request, obj)` to choose inlines per request, for example hiding a block on the add page where there is no parent yet. Django locates the child's `ForeignKey` to the parent. If there is none, or more than one (a `Lesson` with both `course` and `prerequisite_course`), the admin's system checks fail at startup with `admin.E202`, whose message tells you to specify **`fk_name`**, the field to use. ## Tabular or stacked | | `TabularInline` | `StackedInline` | |---|---|---| | Layout | one table row per child | one full form block per child | | Good for | a few short fields: title, position, duration | long text, many fields, fieldsets | | Readability with many children | high, scannable | low, long page | | Mechanics | identical | identical | The Django docs say it plainly: the difference between the two is **merely the template**. Everything else, validation, saving and options, is shared. ## Options that matter | Option | Default | Effect | |---|---|---| | `extra` | `3` | blank forms shown in addition to existing children | | `min_num` | `None` | minimum number of forms | | `max_num` | `None` | maximum number of forms; hides "Add another" when reached | | `can_delete` | `True` | a delete checkbox per child | | `show_change_link` | `False` | a link to the child's own change form, if it is registered | | `fk_name` | `None` | which foreign key to the parent to use | | `classes` | `None` | CSS classes for the block, e.g. `["collapse"]` | Inlines also share most `ModelAdmin` options: `fields`, `fieldsets`, `readonly_fields`, `ordering`, `autocomplete_fields`, `raw_id_fields`, `prepopulated_fields`, `get_queryset()` and the `formfield_for_*` hooks. `raw_id_fields` and `autocomplete_fields` matter as much in inlines as on the main form, because every inline row repeats the widget. ## Practical guidance - Set `extra = 0` or `1` when the course already has lessons; three empty rows on every save is noise and occasionally leads to half-filled rows failing validation. - Put an `ordering = ["position"]` on the inline so lessons appear in teaching order. - Keep inline row counts modest. A course with 400 lessons renders 400 forms, each with its own widgets, and a single POST submits all of them. Past a few dozen children, `show_change_link` plus a filtered change list for lessons is usually better. - Inlines do not nest: a lesson's attachments cannot be an inline inside the lesson inline on the course page. Give lessons their own `ModelAdmin` with an attachments inline and link to it. - Many-to-many relations can be edited inline through their `through` model (`model = Course.tags.through`). ## What an inline costs to render Every existing child becomes a form, and every form renders its own widgets. Three consequences follow for a course page: - A `ForeignKey` column in the inline (say, a lesson `reviewer`) renders a full select of every user **per lesson row**; switch it to `autocomplete_fields` or `raw_id_fields` on the inline. - A child `__str__()` or read-only method that touches another relation runs per row; override the inline's `get_queryset()` to join what it needs. - The POST carries every field of every row, and `DATA_UPLOAD_MAX_NUMBER_FIELDS` (1000 by default) caps how many fields one request may submit, so very large inlines can fail to save as well as render slowly. ## Saving order The parent is saved first, then the inlines (in `save_related()`), all inside one transaction on POST. Code that needs the saved lessons must therefore run after the inlines are saved, not in `save_model()`.

  • Why does a Django admin inline fail when the child model has two ForeignKeys to the parent?
    The inline formset cannot guess which foreign key ties a lesson to the course being edited, so the system check `admin.E202` fails at startup, saying the model has more than one `ForeignKey` to the parent. Setting `fk_name = "course"` on the inline names the field to use.
  • Can a Django admin inline contain another inline, such as lesson attachments inside the lesson inline?
    No, inlines are one level deep: an `InlineModelAdmin` has no `inlines` of its own that the parent page renders. Register `Lesson` with its own `ModelAdmin` carrying an attachments inline, and set `show_change_link = True` on the lesson inline so editors can jump there.

saying these in an interview costs you the question

  • StackedInline validates and saves differently from TabularInline
  • Inlines can be nested to any depth on one change form
  • extra is the maximum number of children an inline allows
  • Inlines work for any related model, even without a ForeignKey to the parent
  • Inlines cannot use readonly_fields or autocomplete_fields