Django Admin

6 min read #Admin #Django #Python

Introduction

Every web app that stores data eventually needs a back office: a place to add records, fix typos, and unpublish the post you regret. Building that interface by hand is dull, repetitive work. Django's answer is to give you one for free. The Django admin reads your models and generates a complete, secure, searchable management site, and this blog's entire writing workflow lives inside it.

The Problem the Admin Solves

Most projects need the same internal tooling, over and over:

  • A list of records you can search, filter, and sort
  • Forms to create and edit each record, with validation
  • Login, permissions, and an audit trail of who changed what
  • A way for non-developers to manage content without touching code

Writing all of that for every model is a lot of work that has nothing to do with what makes your app special. Many teams skip it and end up editing the database by hand, which is risky.

The Django Admin Approach

You register a model, and Django builds the interface from the model's own definition: field types become form widgets, relations become pickers, and validation rules carry over automatically.

Core idea: Your models already describe your data. The admin turns that description into a working management UI, and you customize only the parts that matter.

Simple Example

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

from .models import Post

admin.site.register(Post)

Create a user with python manage.py createsuperuser, run the server, and visit /admin/. You get a list of posts, an add form, an edit form, a delete confirmation, and a change history, from three lines of code.

How This Blog Uses the Admin

The Post model stores Markdown in body_md and renders it to sanitized HTML when you save. The admin is where that happens: you write Markdown, click save, and the model's save() method does the rest. Here is a more tailored admin for a blog like this one:

# blog/admin.py
from django.contrib import admin
from django.utils import timezone
from django.utils.safestring import mark_safe

from .models import Post, Tag


@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ("name", "slug")
    search_fields = ("name",)
    prepopulated_fields = {"slug": ("name",)}


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ("title", "is_published", "published_at", "updated_at")
    list_editable = ("is_published",)
    list_filter = ("is_published", "tags")
    search_fields = ("title", "summary", "body_md")
    prepopulated_fields = {"slug": ("title",)}
    filter_horizontal = ("tags",)
    date_hierarchy = "published_at"
    readonly_fields = ("preview", "created_at", "updated_at")
    actions = ["publish", "unpublish"]

    @admin.display(description="Preview")
    def preview(self, obj):
        # Safe because body_html is sanitized with nh3 when the post is saved.
        return mark_safe(obj.body_html)

    @admin.action(description="Publish selected posts")
    def publish(self, request, queryset):
        updated = queryset.update(is_published=True, updated_at=timezone.now())
        self.message_user(request, f"{updated} post(s) published.")

    @admin.action(description="Unpublish selected posts")
    def unpublish(self, request, queryset):
        updated = queryset.update(is_published=False, updated_at=timezone.now())
        self.message_user(request, f"{updated} post(s) unpublished.")

What's happening:

  • list_display and list_editable turn the post list into a dashboard where you can toggle publishing right from the table
  • list_filter and search_fields add a sidebar filter and a search box with no extra work
  • prepopulated_fields fills in the slug as you type the title, and you can override it before saving
  • filter_horizontal gives tags a friendly two-column picker instead of a long multi-select
  • date_hierarchy adds date drill-down navigation across the top of the list
  • The preview method shows the rendered HTML on the edit page, since body_html is not editable
  • Actions let you publish or unpublish many posts in one click

A Detail Worth Knowing

queryset.update() writes straight to the database and skips the model's save() method. That's why the actions above set updated_at themselves. It also means that if you ever bulk-update body_md this way, body_html would not be re-rendered. For changes that must run save(), loop over the objects and save each one.

Also notice that the model keeps objects as an unfiltered manager. That's what allows the admin to show drafts and future-dated posts, while the public site uses Post.published.

Why This Matters

  1. Instant productivity โ€” A usable back office appears the moment your models exist.

  2. Safe by default โ€” Authentication, permissions, CSRF protection, and form validation come built in.

  3. Customizable in small steps โ€” Start with register() and add options only when you need them.

  4. Built-in accountability โ€” The history view records who changed each object, and when.

  5. A content workflow with no extra code โ€” Authors write in forms; developers don't build editors.

Key ModelAdmin Options

Option Purpose Example
list_display Columns in the list view ("title", "published_at")
list_filter Sidebar filters ("is_published", "tags")
search_fields Fields the search box queries ("title", "summary")
prepopulated_fields Auto-fill a field from others {"slug": ("title",)}
readonly_fields Show values without editing them ("created_at",)
actions Bulk operations on selected rows ["publish"]
date_hierarchy Date drill-down navigation "published_at"

Real-World Use Cases

The admin is a good fit for:

  • Content management โ€” Posts, pages, tags, and categories, as on this blog
  • Internal tools โ€” Support teams viewing and correcting customer records
  • Moderation โ€” Approving comments, hiding listings, banning accounts
  • Early-stage products โ€” A working back office before you have time to build a custom one
  • Data fixes โ€” Safe, validated edits instead of raw SQL

Getting Started

  1. Check that it's enabled. django.contrib.admin is in INSTALLED_APPS by default, and path("admin/", admin.site.urls) is in your root urls.py.

  2. Apply migrations and create a user:

    bash python manage.py migrate python manage.py createsuperuser

  3. Register a model in the app's admin.py.

  4. Open /admin/ and add a record.

  5. Customize one thing at a time: add list_display, then search_fields, then an action.

Locking It Down in Production

The admin is powerful, so treat it like the front door of your back office:

  • Use HTTPS only, and set secure cookie settings such as SESSION_COOKIE_SECURE = True and CSRF_COOKIE_SECURE = True
  • Use strong, unique passwords for every staff account, and give people only the permissions they need
  • Consider moving the URL from /admin/ to something less obvious, or restricting it by IP address in your Nginx config
  • Keep Django updated, since security fixes ship regularly
  • Add two-factor authentication with a third-party package if several people have access

Know the Limits

  • It's an internal tool, not a customer interface. Don't expose it to end users.
  • Complex workflows get awkward. Multi-step processes and heavily customized screens are usually better as dedicated views.
  • Large tables need care. Unindexed filters and huge list_display queries can slow down the list page.
  • The design is functional, not branded. It can be themed, but it's meant to be practical, not pretty.

The Philosophy

The Django admin embodies a simple idea: boring, repetitive work should be generated, not written. By turning your models into a management interface automatically, it lets you spend your effort on the parts of the product that are actually yours.

Register your models, customize what annoys you, and let the admin be exactly what it's good at: a fast, safe way to look after your data.


This post is part of a series on the tools behind this blog: Django, HTMX, Tailwind CSS, PostgreSQL, SQLite, Docker, Debian on a VPS, tmux, SSH, Neovim, and now the Django admin. Posts are authored in the Django admin as Markdown and rendered to HTML on save.

Read more about the tech stack