In a Django ModelAdmin, how do fields, fieldsets, readonly_fields and prepopulated_fields shape the change form?
answer
- which inputs, in which order
- named groups with options
- shown but not submitted
- slug typed for you in the browser
basics
~20 sfields picks and orders the inputs; fieldsets groups them under headings with classes and descriptions; readonly_fields shows values as text and removes them from the form; prepopulated_fields fills a slug from other fields with JavaScript.
solid answer
~40 s`fields` chooses which model fields appear and in what order; wrapping names in a tuple puts them on one line. `fieldsets` replaces it for richer layouts: a list of `(name, {"fields": [...], "classes": [...], "description": ...})`, where `classes=["collapse"]` makes a named section start collapsed. Setting both is a system-check error. `readonly_fields` renders values as text and **excludes them from the ModelForm**, so they are never submitted; it also accepts model or `ModelAdmin` methods for computed values, and it is how a non-editable field such as an `auto_now_add` timestamp appears at all. With `fields` or `fieldsets` set, read-only entries must be listed there too. `prepopulated_fields = {"slug": ["title"]}` fills the slug in the browser as the title is typed, and stops once a value is saved.
code
python · 24 linesfrom django.contrib import admin
from courses.models import Course
@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ["title"]}
readonly_fields = ["created_by", "created_at", "lesson_count"]
fieldsets = [
(None, {"fields": ["title", "slug", "summary"]}),
("Catalogue", {"fields": [("category", "level"), "price"]}),
(
"Audit",
{
"classes": ["collapse"],
"fields": ["created_by", "created_at", "lesson_count"],
},
),
]
@admin.display(description="Lessons")
def lesson_count(self, obj):
return obj.lessons.count() if obj.pk else 0go deeper
Know that fields picks and orders inputs, fieldsets groups them, readonly_fields shows values as text, and prepopulated_fields fills slugs.
Explain the fields/fieldsets exclusivity, collapse, why non-editable fields must be read-only, and that read-only fields are excluded from the form.
Show you rely on server-side read-only exclusion for audit data, avoid mutating readonly_fields per request, and never trust client-side slug generation.
Decide what editors may change in the admin at all, and which data must be immutable outside controlled workflows.
## The default form With no layout options, the change form shows every field that is not an `AutoField` and has `editable=True`, in model definition order, in one unnamed section, followed by any `readonly_fields`. For a `Course` that is fine for a prototype and poor for editors: the SEO slug sits next to the price, and the audit timestamps are missing because they are not editable. ## fields: which inputs and in what order `fields = ["title", "slug", ("category", "level"), "summary"]` shows exactly those fields in that order; the inner tuple puts `category` and `level` on one line. Names of model or `ModelAdmin` methods are only accepted if they are also in `readonly_fields`. A model field declared `editable=False` (including `auto_now` and `auto_now_add` timestamps) cannot be an input: listing it in `fields` without making it read-only makes the form factory raise `FieldError` about a non-editable field. ## fieldsets: grouped sections `fieldsets` is a list of 2-tuples `(name, options)`. `name` is the section heading or `None`; `options` is a dict with: - `"fields"`: the same syntax as `fields`, required; - `"classes"`: extra CSS classes; Django's own `collapse` makes a **named** section start collapsed behind a toggle (rendered with `<details>`/`<summary>` since 5.1); - `"description"`: text under the heading, which is **not HTML-escaped**, so never build it from user data. `fields` and `fieldsets` are alternatives: defining both fails the admin's system checks (`admin.E005`). The `wide` class was removed in Django 6.1 because the new form layout made it obsolete. ## readonly_fields: shown, not submitted | Behaviour | Effect | |---|---| | Rendering | the value is displayed as text, not an input | | Form | the field is **excluded from the ModelForm**, so a POST cannot change it | | Placement | appended after editable fields, unless `fields`/`fieldsets` place it (then it must be listed there, or it is not shown) | | Methods | model or `ModelAdmin` methods can be listed, producing computed rows such as "Enrolled students" | The second row is the security-relevant one: making a field read-only in the admin is enforced server-side, not just hidden in the page. For per-request rules (read-only for everyone except superusers), override `get_readonly_fields(request, obj)` and return a **new** list; appending to the class attribute mutates it for every later request. ## prepopulated_fields: slugs typed for you `prepopulated_fields = {"slug": ["title"]}` adds JavaScript that concatenates the source fields and turns the result into a valid slug (dashes for spaces, lower-case ASCII) as the editor types. Three limits: 1. It is **client-side only**. Objects created through the shell, an API or a fixture get no slug from it. 2. It stops once a value has been saved, so published URLs do not change when a title is edited. 3. The target cannot be a `DateTimeField`, `ForeignKey`, `OneToOneField` or `ManyToManyField`. ## fields versus exclude `exclude` is the negative form of `fields`: list what to hide and show the rest. It is convenient for one or two internal columns but brittle, because a field added to the model later appears in the admin automatically, possibly one that should never be editable (a payment reference, an external id). Listing `fields` or `fieldsets` explicitly means new columns stay hidden until someone decides where they belong. `get_exclude(request, obj)` exists for per-request variations, and read-only fields are appended to the exclusion list either way. ## A course form that editors like - A top section with `title`, `slug` and `summary`, slug prepopulated from title. - A "Catalogue" section with category, level and price. - A collapsed "Audit" section listing `created_by` and `created_at`, both read-only. The result is a form with no duplicated inputs, no accidental slug changes after publication, and audit data that cannot be edited through the admin.
- Why does adding created_at (auto_now_add) to a Django ModelAdmin's fields raise FieldError?`auto_now_add` makes the field non-editable, and a ModelForm cannot include a non-editable field as an input. The admin builds its form from `fields`, so the factory raises `FieldError`. Add `created_at` to `readonly_fields` as well; it is then displayed as text and excluded from the form.
- In Django's admin, is a readonly_fields value protected against a crafted POST?Yes. Read-only fields are excluded from the `ModelForm` the admin builds, so a value posted for them is ignored rather than saved. That differs from disabling an input in HTML alone. Values set in `save_model()` or by the model itself still apply, which is how `created_by` gets filled.
saying these in an interview costs you the question
- readonly_fields only greys out the input; a crafted POST can still change it
- fields and fieldsets can both be set and Django merges them
- prepopulated_fields generates slugs for objects created in the shell too
- An auto_now_add field can be listed in fields like any other field
- A fieldset description is escaped, so user data is safe there