Django Admin 進階:Inline 編輯、自訂 Actions 與權限控制 | Django 教學

2026/06/09 2026/05/22
Django Admin 進階:Inline 編輯、自訂 Actions 與權限控制 | Django 教學

當你已經能在 Django Admin 中註冊 Model 並進行基本的 CRUD 操作後,下一步就是深入掌握 Admin 的進階功能。本篇將帶你學會 Inline 編輯 在同一頁面管理關聯資料、透過自訂 Actions 實現批次操作、使用 fieldsets 分組整理欄位,以及透過 權限控制(Permission Control) 精確管理使用者的存取範圍,最後再介紹幾個實用的 效能優化(Performance Optimization) 技巧。

Inline 模型編輯

在 Django Admin 中,Inline(內嵌編輯) 讓你可以在父模型的編輯頁面中,直接新增、修改或刪除關聯的子模型資料,不需要跳轉到另一個頁面。例如:在編輯一篇 Post 時,同時管理它底下的所有 Comment。

Django 提供兩種 Inline 類型:TabularInline(表格式)與 StackedInline(堆疊式)。

TabularInline(表格式)

TabularInline 以表格形式呈現,每筆關聯資料佔一列,畫面較為精簡,適合欄位數量較少的子模型。

from django.contrib import admin
from .models import Post, Comment

# 表格式 Inline:適合欄位少的關聯模型
class CommentTabularInline(admin.TabularInline):
    model = Comment
    extra = 1          # 預設顯示 1 個空白列供新增
    max_num = 20       # 最多允許 20 筆留言
    min_num = 0        # 最少 0 筆(非必填)
    fields = ['author', 'content', 'is_approved', 'created_at']
    readonly_fields = ['created_at']

StackedInline(堆疊式)

StackedInline 以堆疊方式呈現,每筆資料的欄位獨立成一個區塊,適合欄位較多或需要較大編輯空間的情境。

# 堆疊式 Inline:適合欄位多、需要較大編輯空間的關聯模型
class CommentStackedInline(admin.StackedInline):
    model = Comment
    extra = 0              # 不顯示額外空白列
    max_num = 50           # 最多允許 50 筆
    show_change_link = True  # 顯示前往子模型編輯頁的連結
    fields = ['author', 'content', 'is_approved', 'created_at']
    readonly_fields = ['created_at', 'author']

將 Inline 掛載到 ModelAdmin

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'is_published', 'created_at']
    # 在 Post 編輯頁面內嵌 Comment
    inlines = [CommentTabularInline]

設定完成後,在 Admin 中編輯任何一篇 Post 時,頁面下方就會出現 Comment 的內嵌表格,可以直接新增、修改或刪除留言,全部在同一個頁面完成。

extramax_nummin_num 這三個參數用來控制 Inline 的數量行為:

參數說明預設值
extra額外顯示的空白列數3
max_num最多允許的物件總數無限制
min_num最少需要的物件數量0

自訂 Admin Actions

Actions(動作) 是 Django Admin 列表頁上方的下拉選單功能,讓你對勾選的多筆資料執行批次操作。Django 預設提供「刪除選取的項目」這個 Action,但你可以自訂任意數量的 Action。

定義 Action 函數與 @admin.action 裝飾器

from django.contrib import admin
from django.contrib import messages
from django.utils import timezone
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'is_published', 'created_at']
    actions = ['make_published', 'make_draft']

    # 使用 @admin.action 裝飾器定義動作
    @admin.action(description='將選取的文章標記為已發布')
    def make_published(self, request, queryset):
        # queryset 包含使用者勾選的所有物件
        count = queryset.update(
            is_published=True,
            published_at=timezone.now()
        )
        self.message_user(
            request,
            f'已成功發布 {count} 篇文章。',
            messages.SUCCESS
        )

    @admin.action(description='將選取的文章退回草稿')
    def make_draft(self, request, queryset):
        count = queryset.update(
            is_published=False,
            published_at=None
        )
        self.message_user(
            request,
            f'已將 {count} 篇文章退回草稿。',
            messages.WARNING
        )

批次匯出 CSV 範例

自訂 Action 不限於修改資料,也可以產生檔案下載。以下示範匯出 CSV 的實作:

import csv
from django.http import HttpResponse

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    actions = ['export_as_csv']

    @admin.action(description='匯出選取的文章為 CSV')
    def export_as_csv(self, request, queryset):
        # 建立 CSV 回應
        response = HttpResponse(content_type='text/csv')
        response['Content-Disposition'] = 'attachment; filename="posts.csv"'

        writer = csv.writer(response)
        # 寫入標題列
        writer.writerow(['ID', '標題', '作者', '發布狀態', '建立時間'])
        # 寫入資料列
        for post in queryset:
            writer.writerow([
                post.id,
                post.title,
                post.author.username,
                '已發布' if post.is_published else '草稿',
                post.created_at.strftime('%Y-%m-%d %H:%M'),
            ])

        self.message_user(
            request,
            f'已匯出 {queryset.count()} 篇文章。',
            messages.SUCCESS
        )
        return response

fieldsets 分組顯示欄位

當 Model 的欄位數量較多時,fieldsets(欄位分組)能讓編輯頁面更有組織性。每個分組可以設定標題、包含的欄位,以及 CSS 類別(如 collapse 預設摺疊)。

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    fieldsets = [
        ('基本資訊', {
            'fields': ['title', 'slug', 'author', 'category'],
        }),
        ('文章內容', {
            'fields': ['content', 'cover_image'],
        }),
        ('發布設定', {
            'fields': ['is_published', 'published_at', 'tags'],
            'classes': ['collapse'],  # 預設摺疊,點擊才展開
            'description': '控制文章的發布狀態與分類標籤',
        }),
        ('系統資訊(唯讀)', {
            'fields': ['view_count', 'created_at', 'updated_at'],
            'classes': ['collapse'],
        }),
    ]

readonly_fields 與 get_readonly_fields()

readonly_fields 讓你將特定欄位設定為唯讀(Read-only),在 Admin 編輯頁面中只能檢視、不能修改。

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # 靜態唯讀欄位
    readonly_fields = ['created_at', 'updated_at', 'view_count']

若需要根據使用者身份動態決定哪些欄位為唯讀,可以覆寫 get_readonly_fields() 方法:

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    readonly_fields = ['created_at', 'updated_at']

    def get_readonly_fields(self, request, obj=None):
        # 取得基本唯讀欄位
        readonly = list(super().get_readonly_fields(request, obj))
        # 非超級使用者不能修改作者欄位
        if not request.user.is_superuser:
            readonly.append('author')
        # 已發布的文章,標題不可修改
        if obj and obj.is_published:
            readonly.append('title')
        return readonly

Admin 權限控制

Django Admin 內建了完整的 權限系統(Permission System),透過覆寫四個 has_*_permission 方法,可以精確控制每位使用者的新增、修改、刪除與檢視權限。

has_add_permission / has_change_permission / has_delete_permission / has_view_permission

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    def has_add_permission(self, request):
        """控制誰可以新增文章"""
        # 只有 Editor 群組的成員可以新增
        return request.user.groups.filter(name='Editor').exists()

    def has_change_permission(self, request, obj=None):
        """控制誰可以編輯文章"""
        if obj is None:
            # obj 為 None 表示列表頁的整體權限
            return True
        # 只有作者本人或超級使用者可以編輯
        return obj.author == request.user or request.user.is_superuser

    def has_delete_permission(self, request, obj=None):
        """控制誰可以刪除文章"""
        # 只有超級使用者可以刪除
        return request.user.is_superuser

    def has_view_permission(self, request, obj=None):
        """控制誰可以檢視文章(Django 2.1+)"""
        # 所有已登入的管理員都可以檢視
        return True

get_queryset() 過濾使用者可見資料

除了權限控制,有時你還需要根據使用者身份過濾可見的資料範圍。覆寫 get_queryset() 是最直接的做法:

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        # 超級使用者可以看到所有文章
        if request.user.is_superuser:
            return qs
        # 一般管理員只能看到自己建立的文章
        return qs.filter(author=request.user)

這種做法確保使用者連列表頁都只會看到自己有權限存取的資料,搭配 has_change_permissionhas_delete_permission 可以建立完整的資料隔離機制。


自訂 Admin 模板

Django Admin 基於標準的 Django 模板系統,可以透過覆寫模板來客製化頁面外觀與行為。

change_list_template 與 change_form_template

ModelAdmin 中指定自訂模板路徑,即可覆寫特定 Model 的列表頁或編輯頁:

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # 自訂列表頁模板
    change_list_template = 'admin/myapp/post/change_list.html'
    # 自訂編輯頁模板
    change_form_template = 'admin/myapp/post/change_form.html'

自訂模板通常繼承原始模板,只覆寫需要修改的區塊:

{# templates/admin/myapp/post/change_list.html #}
{% extends "admin/change_list.html" %}

{% block content %}
  <div style="padding: 10px; background: #f0f8ff; border-radius: 4px; margin-bottom: 15px;">
    <strong>提示:</strong>本頁面顯示所有文章,點擊標題可進入編輯。
  </div>
  {{ block.super }}
{% endblock %}

確保 settings.py 中的 TEMPLATES 設定包含你的自訂模板目錄:

# settings.py
TEMPLATES = [{
    'DIRS': [BASE_DIR / 'templates'],  # 自訂模板目錄
    'APP_DIRS': True,
    # ...
}]

Admin 效能優化

當資料量增長後,Admin 列表頁可能會出現載入緩慢的問題,通常是因為 N+1 查詢問題(N+1 Query Problem) 或一次載入過多關聯資料。以下是三個最實用的優化技巧。

list_select_related 告訴 Django 在載入列表頁時,自動對指定的 ForeignKey 欄位執行 select_related,將多次查詢合併為一次 JOIN 查詢。

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'category', 'created_at']
    # 自動 select_related,避免列表頁對每筆資料額外查詢 author 和 category
    list_select_related = ['author', 'category']

raw_id_fields(大量 ForeignKey)

ForeignKey 關聯的目標資料表有成千上萬筆資料時,預設的下拉選單會載入所有選項,導致頁面極度緩慢。raw_id_fields 將下拉選單替換為一個 ID 輸入框搭配彈出式搜尋視窗。

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # 將 author 欄位改為 ID 輸入框 + 放大鏡搜尋
    raw_id_fields = ['author']

autocomplete_fields

autocomplete_fieldsraw_id_fields 的現代替代方案(Django 2.0+),提供自動完成(Autocomplete)的搜尋體驗。使用前,關聯模型的 ModelAdmin 必須設定 search_fields

from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

# 確保關聯模型有設定 search_fields
class CustomUserAdmin(UserAdmin):
    search_fields = ['username', 'email', 'first_name']

# 取消原有註冊,重新註冊
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # 自動完成搜尋,體驗比 raw_id_fields 更好
    autocomplete_fields = ['author', 'category']

三種方式的比較:

方式適用場景使用體驗
預設下拉選單選項少於 100 筆最直覺,但資料多時會卡頓
raw_id_fields大量資料、不需搜尋ID 輸入框 + 彈出視窗
autocomplete_fields大量資料、需要搜尋輸入關鍵字即時搜尋,推薦使用

總結

Django Admin 的進階功能讓你可以打造出高效且符合業務需求的後台管理介面。以下整理本篇的核心要點:

  1. Inline 編輯 讓你在同一頁面管理父子關聯資料,TabularInline 適合欄位少的情境,StackedInline 適合欄位多的情境,善用 extramax_nummin_num 控制數量
  2. 自訂 Actions 能實現批次發布、匯出 CSV 等常見操作,透過 @admin.action 裝飾器定義,搭配 self.message_user() 回饋操作結果
  3. fieldsets 與 readonly_fields 讓編輯頁面更有組織性,get_readonly_fields() 可根據使用者身份或物件狀態動態調整唯讀欄位
  4. 權限控制 透過四個 has_*_permission 方法精確管理存取權限,搭配 get_queryset() 過濾可見資料,建立完整的資料隔離機制
  5. 效能優化 使用 list_select_related 解決 N+1 查詢問題,大量 ForeignKey 資料改用 autocomplete_fields 取代預設下拉選單

掌握這些進階技巧後,你的 Django Admin 後台將不再只是「能用」的基本工具,而是一套「好用」的管理系統。下一篇我們將探討 Django 的 靜態檔案管理(Static Files)媒體檔案處理(Media Files),學習如何正確管理 CSS、JavaScript 與使用者上傳的檔案。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。