跳轉到

Django Admin 系統

開始之前

任務目標

在這個章節中,我們會完成:

  • 了解 Django Admin 的功能
  • 建立超級使用者
  • 註冊 Model 到 Admin
  • 自訂 Admin 介面

什麼是 Django Admin?

Django Admin 是 Django 內建的後台管理系統,讓你可以透過網頁介面管理資料,不需要寫任何前端程式碼。

特色

特色 說明
自動生成 根據 Model 自動產生管理介面
功能完整 新增、編輯、刪除、搜尋、篩選
權限管理 內建使用者權限系統
可自訂 可以自訂顯示欄位、篩選器、動作

適用場景

Django Admin 非常適合:

  • 內部管理系統
  • 快速原型開發
  • 資料維護工具

但不適合用作面向使用者的介面,因為它是為管理員設計的。

建立超級使用者

在使用 Admin 之前,需要先建立一個管理員帳號。

執行指令

uv run manage.py createsuperuser

輸入資訊

系統會詢問你以下資訊:

Username (leave blank to use 'user'): admin
Email address: [email protected]
Password:
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

密碼規則

Django 預設會檢查密碼強度:

  • 不能太短(至少 8 個字元)
  • 不能太常見
  • 不能全是數字

如果密碼太簡單,Django 會警告你,但仍然可以選擇繼續使用(開發環境)。

啟動伺服器並登入

uv run manage.py runserver

開啟瀏覽器訪問 http://127.0.0.1:8000/admin/,輸入剛才建立的帳號密碼。

登入成功

登入後你會看到 Django Admin 的首頁,預設已經有 UsersGroups 兩個管理項目。

註冊 Model 到 Admin

要讓 Model 出現在 Admin 中,需要在 admin.py 中註冊。

基本註冊

blog/admin.py 中:

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag

admin.site.register(Article)
admin.site.register(Author)
admin.site.register(Tag)

重新整理 Admin 頁面,現在你可以看到 ArticlesAuthorsTags 三個管理項目。

預設行為

使用 admin.site.register() 註冊後,Django 會:

  • 顯示 Model 的所有欄位
  • 提供新增、編輯、刪除功能
  • 顯示 __str__() 方法的回傳值

自訂 Admin 介面

雖然預設的 Admin 已經很好用,但我們可以透過 ModelAdmin 來自訂更多功能。

ModelAdmin 基本用法

建立一個繼承自 ModelAdmin 的 class,並在註冊時指定:

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


@admin.register(Author)  # (1)!
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]  # (2)!


admin.site.register(Article)
admin.site.register(Tag)
  1. 使用 decorator 註冊,等同於 admin.site.register(Author, AuthorAdmin)
  2. 設定列表頁要顯示的欄位

重新整理 Admin 頁面,點擊 Authors,現在列表會顯示 name、email、created_at 三個欄位。

常用的 ModelAdmin 屬性

list_display - 列表顯示欄位

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published", "created_at"]


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]


admin.site.register(Tag)

list_filter - 側邊篩選器

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published", "created_at"]
    list_filter = ["is_published", "created_at", "author"]  # (1)!


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]


admin.site.register(Tag)
  1. 右側會出現篩選器,可以按照這些欄位篩選

篩選器類型

Django 會根據欄位類型自動選擇適合的篩選器:

  • BooleanField → 是/否
  • DateTimeField → 日期範圍
  • ForeignKey → 關聯物件列表

search_fields - 搜尋欄位

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published", "created_at"]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]  # (1)!


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]


admin.site.register(Tag)
  1. 上方會出現搜尋框,可以搜尋 title 或 content 欄位

ordering - 預設排序

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published", "created_at"]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]  # (1)!


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]


admin.site.register(Tag)
  1. 預設按 created_at 降冪排序(最新的在最上面)

list_per_page - 每頁顯示數量

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published", "created_at"]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20  # (1)!


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]


admin.site.register(Tag)
  1. 預設是 100,這裡改成每頁顯示 20 筆

Inline 編輯

對於有關聯的 Model,可以使用 Inline 在同一個頁面編輯相關資料。

TabularInline - 表格式編輯

適合欄位較少的情況:

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.TabularInline):  # (1)!
    model = Article
    extra = 1  # (2)!
    fields = ["title", "is_published"]  # (3)!


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published", "created_at"]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]
    inlines = [ArticleInline]  # (4)!


admin.site.register(Tag)
  1. 建立一個 Inline class 繼承自 TabularInline
  2. 預設顯示幾個空白表單(用於新增)
  3. 指定要顯示的欄位
  4. 在 AuthorAdmin 中使用這個 Inline

現在編輯作者時,可以直接在同一頁面新增/編輯他的文章。

StackedInline - 堆疊式編輯

適合欄位較多的情況:

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):  # (1)!
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author", "is_published", "created_at"]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]
    inlines = [ArticleInline]


admin.site.register(Tag)
  1. 改用 StackedInline,欄位會垂直排列

TabularInline vs StackedInline

類型 適用場景
TabularInline 欄位少、需要一次看多筆
StackedInline 欄位多、需要詳細編輯

自訂顯示欄位

除了顯示 Model 的欄位,還可以自訂顯示內容。

使用方法顯示自訂內容

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "author",
        "is_published",
        "created_at",
        "tag_count",  # (1)!
    ]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20

    @admin.display(description="標籤數量")  # (2)!
    def tag_count(self, obj):  # (3)!
        return obj.tags.count()


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]
    inlines = [ArticleInline]


admin.site.register(Tag)
  1. 在 list_display 中使用自訂方法
  2. 使用 decorator 設定欄位標題
  3. obj 是當前的 Article 物件

顯示布林值圖示

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "author",
        "is_published",
        "created_at",
        "tag_count",
    ]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20

    @admin.display(description="標籤數量")
    def tag_count(self, obj):
        return obj.tags.count()


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at", "has_published_articles"]  # (1)!
    inlines = [ArticleInline]

    @admin.display(description="有已發布的文章", boolean=True)  # (2)!
    def has_published_articles(self, obj):  # (3)!
        return obj.articles.filter(is_published=True).exists()


admin.site.register(Tag)
  1. 在 list_display 中加入自訂方法
  2. boolean=True 會顯示 ✓ 或 ✗ 圖示
  3. 檢查作者是否有已發布的文章

自訂 Actions

Admin 提供批次操作功能,可以一次對多筆資料執行動作。

建立自訂 Action

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "author",
        "is_published",
        "created_at",
        "tag_count",
    ]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20
    actions = ["publish_articles", "unpublish_articles"]  # (1)!

    @admin.display(description="標籤數量")
    def tag_count(self, obj):
        return obj.tags.count()

    @admin.action(description="發布選中的文章")  # (2)!
    def publish_articles(self, request, queryset):  # (3)!
        count = queryset.update(is_published=True)  # (4)!
        self.message_user(request, f"成功發布 {count} 篇文章")  # (5)!

    @admin.action(description="取消發布選中的文章")
    def unpublish_articles(self, request, queryset):
        count = queryset.update(is_published=False)
        self.message_user(request, f"成功取消發布 {count} 篇文章")


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at", "has_published_articles"]
    inlines = [ArticleInline]

    @admin.display(description="有已發布的文章", boolean=True)
    def has_published_articles(self, obj):
        return obj.articles.filter(is_published=True).exists()


admin.site.register(Tag)
  1. 註冊自訂 actions
  2. 使用 decorator 設定動作顯示名稱
  3. request 是當前請求,queryset 是選中的物件
  4. 批次更新選中的文章
  5. 顯示成功訊息給使用者

現在在文章列表頁:

  1. 勾選要操作的文章
  2. 在上方的 Action 下拉選單選擇「發布選中的文章」
  3. 點擊「Go」按鈕

Action 的用途

自訂 Action 適合用於:

  • 批次更新資料
  • 批次匯出資料
  • 批次發送通知
  • 批次刪除(可以加上確認步驟)

ManyToMany 欄位處理

對於 ManyToManyField,Admin 預設使用多選框,但資料多時不太好用。

使用 filter_horizontal

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "author",
        "is_published",
        "created_at",
        "tag_count",
    ]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20
    actions = ["publish_articles", "unpublish_articles"]
    filter_horizontal = ["tags"]  # (1)!

    @admin.display(description="標籤數量")
    def tag_count(self, obj):
        return obj.tags.count()

    @admin.action(description="發布選中的文章")
    def publish_articles(self, request, queryset):
        count = queryset.update(is_published=True)
        self.message_user(request, f"成功發布 {count} 篇文章")

    @admin.action(description="取消發布選中的文章")
    def unpublish_articles(self, request, queryset):
        count = queryset.update(is_published=False)
        self.message_user(request, f"成功取消發布 {count} 篇文章")


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at", "has_published_articles"]
    inlines = [ArticleInline]

    @admin.display(description="有已發布的文章", boolean=True)
    def has_published_articles(self, obj):
        return obj.articles.filter(is_published=True).exists()


admin.site.register(Tag)
  1. 使用水平雙欄選擇器,左右欄位可以互相移動

使用 filter_vertical

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "author",
        "is_published",
        "created_at",
        "tag_count",
    ]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20
    actions = ["publish_articles", "unpublish_articles"]
    filter_vertical = ["tags"]  # (1)!

    @admin.display(description="標籤數量")
    def tag_count(self, obj):
        return obj.tags.count()

    @admin.action(description="發布選中的文章")
    def publish_articles(self, request, queryset):
        count = queryset.update(is_published=True)
        self.message_user(request, f"成功發布 {count} 篇文章")

    @admin.action(description="取消發布選中的文章")
    def unpublish_articles(self, request, queryset):
        count = queryset.update(is_published=False)
        self.message_user(request, f"成功取消發布 {count} 篇文章")


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at", "has_published_articles"]
    inlines = [ArticleInline]

    @admin.display(description="有已發布的文章", boolean=True)
    def has_published_articles(self, obj):
        return obj.articles.filter(is_published=True).exists()


admin.site.register(Tag)
  1. 垂直版本的雙欄選擇器

filter_horizontal vs filter_vertical

兩者功能相同,只是排列方向不同:

  • filter_horizontal:左右排列(適合標籤較少)
  • filter_vertical:上下排列(適合標籤較多)

進階設定

隱藏欄位

使用 excludefields 參數:

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "author",
        "is_published",
        "created_at",
        "tag_count",
    ]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20
    actions = ["publish_articles", "unpublish_articles"]
    filter_vertical = ["tags"]
    exclude = ["is_published"]  # (1)!

    @admin.display(description="標籤數量")
    def tag_count(self, obj):
        return obj.tags.count()

    @admin.action(description="發布選中的文章")
    def publish_articles(self, request, queryset):
        count = queryset.update(is_published=True)
        self.message_user(request, f"成功發布 {count} 篇文章")

    @admin.action(description="取消發布選中的文章")
    def unpublish_articles(self, request, queryset):
        count = queryset.update(is_published=False)
        self.message_user(request, f"成功取消發布 {count} 篇文章")


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at", "has_published_articles"]
    inlines = [ArticleInline]

    @admin.display(description="有已發布的文章", boolean=True)
    def has_published_articles(self, obj):
        return obj.articles.filter(is_published=True).exists()


admin.site.register(Tag)
  1. 隱藏 updated_at 欄位,在編輯頁面不會顯示

exclude vs fields

兩種方式都可以控制顯示的欄位:

  • exclude:排除不想顯示的欄位
  • fields:只顯示指定的欄位

建議使用 fields 明確列出要顯示的欄位,比較不容易遺漏重要欄位。

唯讀欄位

使用 readonly_fields

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "author",
        "is_published",
        "created_at",
        "tag_count",
    ]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20
    actions = ["publish_articles", "unpublish_articles"]
    filter_vertical = ["tags"]
    exclude = ["is_published"]
    readonly_fields = ["created_at"]  # (1)!

    @admin.display(description="標籤數量")
    def tag_count(self, obj):
        return obj.tags.count()

    @admin.action(description="發布選中的文章")
    def publish_articles(self, request, queryset):
        count = queryset.update(is_published=True)
        self.message_user(request, f"成功發布 {count} 篇文章")

    @admin.action(description="取消發布選中的文章")
    def unpublish_articles(self, request, queryset):
        count = queryset.update(is_published=False)
        self.message_user(request, f"成功取消發布 {count} 篇文章")


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at", "has_published_articles"]
    inlines = [ArticleInline]

    @admin.display(description="有已發布的文章", boolean=True)
    def has_published_articles(self, obj):
        return obj.articles.filter(is_published=True).exists()


admin.site.register(Tag)
  1. created_at 欄位會顯示但無法編輯

自動時間欄位

如果使用 auto_now_add=Trueauto_now=True 的欄位,Django 會自動設為唯讀,不需要特別設定 readonly_fields

Django Admin 預設隱藏了這些欄位,想要顯示它們,就需要同時設定 readonly_fields

自訂表單佈局

使用 fieldsets

blog/admin.py
from django.contrib import admin

from blog.models import Article, Author, Tag


class ArticleInline(admin.StackedInline):
    model = Article
    extra = 1
    fields = ["title", "content", "is_published"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "title",
        "author",
        "is_published",
        "created_at",
        "tag_count",
    ]
    list_filter = ["is_published", "created_at", "author"]
    search_fields = ["title", "content"]
    ordering = ["-created_at"]
    list_per_page = 20
    actions = ["publish_articles", "unpublish_articles"]
    filter_vertical = ["tags"]
    fieldsets = [  # (1)!
        ("基本資訊", {"fields": ["title", "content"]}),
        (
            "進階選項",
            {
                "fields": ["author", "tags", "is_published"],
                "classes": ["collapse"],  # (2)!
            },
        ),
        (
            "時間資訊",
            {
                "fields": ["created_at", "updated_at"],
            },
        ),
    ]
    readonly_fields = ["created_at", "updated_at"]  # (3)!

    @admin.display(description="標籤數量")
    def tag_count(self, obj):
        return obj.tags.count()

    @admin.action(description="發布選中的文章")
    def publish_articles(self, request, queryset):
        count = queryset.update(is_published=True)
        self.message_user(request, f"成功發布 {count} 篇文章")

    @admin.action(description="取消發布選中的文章")
    def unpublish_articles(self, request, queryset):
        count = queryset.update(is_published=False)
        self.message_user(request, f"成功取消發布 {count} 篇文章")


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at", "has_published_articles"]
    inlines = [ArticleInline]

    @admin.display(description="有已發布的文章", boolean=True)
    def has_published_articles(self, obj):
        return obj.articles.filter(is_published=True).exists()


admin.site.register(Tag)
  1. 使用 fieldsets 將欄位分組顯示
  2. "classes": ["collapse"] 讓該區塊預設摺疊
  3. 時間欄位設為唯讀

fieldsets 的好處

使用 fieldsets 可以:

  • 將相關欄位分組,讓表單更有組織
  • 使用 collapse 摺疊次要資訊,讓頁面更簡潔
  • 使用 wide 讓欄位佔用更多空間

注意:不能同時使用 fieldsfieldsets,兩者擇一使用。

常見問題

為什麼我的 Model 沒有出現在 Admin?

檢查以下幾點:

  1. 是否在 admin.py 中註冊了?
  2. App 是否在 INSTALLED_APPS 中?
  3. 有沒有重啟伺服器?

任務結束

完成!

恭喜你完成了這個章節!現在你已經:

  • 了解 Django Admin 的功能
  • 建立超級使用者
  • 註冊 Model 到 Admin
  • 自訂 Admin 介面

補充說明

Django Admin 還有很多進階功能,例如:

  • 自訂 Admin 樣式(覆寫模板)
  • Admin 權限控制(has_add_permissionhas_change_permission 等)
  • 使用第三方套件(如 django-admin-interfacegrappelli
  • 自訂表單驗證

如果想要了解更多,可以參考 Django Admin 官方文件