How do you override a Django admin template for one model only, and which admin templates can only be overridden project-wide?
answer
- directories named after app and model
- extend, then override one block
- a fixed list of per-model templates
- index and login live on the site
basics
~10 sPut the template at templates/admin/<app_label>/<model_name>/<name>.html, extend the admin original and override one block. Only a listed set, such as change_form.html and change_list.html, is looked up per model; base_site.html and others are project-wide only.
solid answer
~40 sFor templates like `change_list.html`, the admin looks in `admin/<app_label>/<model_name>/`, then `admin/<app_label>/`, then `admin/`, so a file in `templates/admin/donations/donation/change_form.html` affects only Donation. Inside it, `{% extends "admin/change_form.html" %}` and override just the block you need, such as `object-tools-items`, rather than copying the whole file. Only a documented list is looked up this way: among them `change_form.html`, `change_list.html`, `delete_confirmation.html`, `object_history.html`, `actions.html`, `pagination.html`, `search_form.html`, `submit_line.html` and `app_index.html`. Everything else, `base_site.html` included, can only be overridden for the whole project in `templates/admin/`. For the index, login and logout pages the docs recommend an `AdminSite` subclass with `index_template`, `login_template` or `logout_template`, and a `ModelAdmin` can name its own template with attributes such as `change_form_template`.
code
html · 10 lines{# templates/admin/donations/donation/change_form.html #}
{% extends "admin/change_form.html" %}
{% load i18n %}
{% block object-tools-items %}
{{ block.super }}
{% if original %}
<li><a href="{% url 'admin:donations_donation_receipt' original.pk %}">{% translate "Print receipt" %}</a></li>
{% endif %}
{% endblock %}go deeper
Recall the templates/admin/<app>/<model>/ directory convention for overriding one model's pages.
Explain the three-step lookup, the per-model template list, and extending the original to override a single block.
Prefer block overrides and ModelAdmin or AdminSite template attributes, and audit overrides on each Django upgrade.
Weigh how much admin markup a team should own against the upgrade cost it adds to every Django release.
## How the admin finds a template When a `ModelAdmin` renders the change form for `Donation` in the `donations` app, it asks the template engine for the **first existing** of: 1. `admin/donations/donation/change_form.html` 2. `admin/donations/change_form.html` 3. `admin/change_form.html` The first two are yours to create; the third ships with `django.contrib.admin`. The model directory is always the **lowercased** model name, which matters on case-sensitive filesystems. Your `templates/` directory must be searched before the admin's own templates — how loaders and `DIRS` are ordered belongs to the template-engine topic; the admin-specific part is the lookup list above. ## Overriding, not replacing Copying `change_form.html` wholesale works until the next Django release changes it. The admin templates are built from **blocks** precisely so you can override one: - `{% extends "admin/change_form.html" %}` at the top of your file; - `{% block object-tools-items %}{{ block.super }}<li>…</li>{% endblock %}` to add a button while keeping the History link; - `{% block after_field_sets %}` or `{% block submit_buttons_bottom %}` for form additions. Extending the admin's own name from a file that is itself found at an app- or model-specific path is fine: the engine resolves `admin/change_form.html` to the generic file. ## The per-app/per-model list The docs list exactly which templates are looked up per app or model: | Group | Templates | |---|---| | Change list | `change_list.html`, `change_list_results.html`, `change_list_object_tools.html`, `actions.html`, `pagination.html`, `search_form.html`, `date_hierarchy.html` | | Change form | `change_form.html`, `change_form_object_tools.html`, `change_form_actions.html`, `submit_line.html`, `prepopulated_fields_js.html` | | Other pages | `delete_confirmation.html`, `object_history.html`, `popup_response.html`, `app_index.html` | Anything not on the list — `base.html`, `base_site.html`, `index.html`, `login.html`, the 404 and 500 pages — is overridden **project-wide** by placing a file in `templates/admin/`. ## Other ways to choose a template - **`ModelAdmin` attributes**: `change_form_template`, `change_list_template`, `add_form_template`, `delete_confirmation_template`, `delete_selected_confirmation_template`, `object_history_template` and `popup_response_template` name a template explicitly for that admin — handy when two admins of the same model need different pages. - **`AdminSite` attributes**: `index_template`, `app_index_template`, `login_template`, `logout_template` and the password-change templates. The docs recommend these over file overrides for the root and login pages. - **Theming**: colours are CSS variables, changed in the `extrastyle` block of an overridden `admin/base.html` or `base_site.html`. ## Practical checklist 1. Decide the scope: one model, one app, or the whole admin. 2. Find the smallest block that covers the change. 3. Extend the original and call `{{ block.super }}` where you add rather than replace. 4. Re-check overrides when upgrading Django; the 6.1 change-form layout changes (labels above fields) show that markup does move between releases. ## A worked example The charity wants an **Export** button on the Donation change list only: 1. Create `templates/admin/donations/donation/change_list.html`. 2. Start it with `{% extends "admin/change_list.html" %}`. 3. Override `{% block object-tools-items %}`, output `{{ block.super }}` to keep "Add donation", then add an `<li>` linking to the export URL. 4. Reload the Donation change list: the button appears; other models in the app are untouched. To show the button on every model in the `donations` app instead, move the same file one level up, to `templates/admin/donations/change_list.html`. ## When an override does not apply - **Wrong directory name** — the model directory must be the lowercased model name, and the app directory the app label, not the Python package path. - **Search order** — the project's template directories must be searched before the admin's bundled templates; with app-level templates, the app providing them must come before `django.contrib.admin` in `INSTALLED_APPS`. - **A `ModelAdmin` template attribute** such as `change_list_template` is set, so the directory lookup never happens. - **Not on the per-model list** — a per-model `base_site.html` is simply ignored. - **Template caching** — in production, templates are cached by the loader, so a changed file needs a restart or deploy to show.
- Why does the Django admin recommend AdminSite.login_template over a templates/admin/login.html file?A file override applies to every admin site in the project and depends on template search order. Setting `login_template` on an `AdminSite` subclass scopes the change to that site, keeps it next to the site's other configuration, and still lets the template extend the admin original.
- When would you set change_form_template on a Django ModelAdmin instead of relying on the directory lookup?When the template choice is per admin rather than per model: two admin sites registering the same model with different pages, or several models sharing one custom form template. The attribute names the template directly and skips the app and model directory lookup.
saying these in an interview costs you the question
- Every admin template can be overridden per model
- The model directory must match the model's class name exactly
- Overriding means copying the whole admin template into the project
- base_site.html can be overridden separately for each app