2026年版 Django Admin カスタマイズ完全ガイド:アクション、フィルター、面接対策

Django 6のAdmin画面をカスタマイズする方法を解説。カスタムアクション、リストフィルター、フィールドセット、インラインの設定方法と面接頻出質問を紹介します。

2026年版 Django Admin カスタマイズ完全ガイド:アクション、フィルター、面接対策

Django Adminのカスタマイズは、標準装備の管理画面を本番環境向けの実用的なバックオフィスツールへと変貌させます。Django 6では、フィルターのファセットカウント、カスタマイズ可能なパスワード変更フォーム、Font Awesomeアイコンなど、管理画面の機能が大幅に強化されました。適切に設定されたAdmin画面は、日常業務において非常に使いやすいものとなります。

重要ポイント

ModelAdminはすべてのAdminカスタマイズの中核となるクラスです。list_displaylist_filtersearch_fieldsfieldsets、カスタムアクションをマスターすれば、Admin設定の90%に対応できます。

ModelAdminの基本:list_displayとsearch_fields

list_displayは、変更リストページに表示されるカラムを制御します。単純なフィールド名だけでなく、計算値を表示するために@admin.display()デコレータで装飾された呼び出し可能オブジェクトも受け入れます。search_fieldsは、デフォルトでicontainsを使用してレコードをフィルタリングする検索ボックスを有効にします。

python
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Article


class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "author_name", "status", "publish_date", "is_recent"]
    search_fields = ["title", "author__email", "content"]
    list_per_page = 50
    list_select_related = ["author"]  # N+1クエリを回避

    @admin.display(description="著者", ordering="author__last_name")
    def author_name(self, obj):
        return f"{obj.author.first_name} {obj.author.last_name}"

    @admin.display(description="最近?", boolean=True)
    def is_recent(self, obj):
        from django.utils import timezone
        return obj.publish_date >= timezone.now() - timezone.timedelta(days=7)


admin.site.register(Article, ArticleAdmin)

@admin.display()デコレータは、関数属性としてshort_descriptionを設定する古いパターンを置き換えます。boolean=Trueパラメータは、True/Falseテキストの代わりにチェックマークまたはXアイコンをレンダリングします。list_select_relatedを使用すると、list_displayで外部キーフィールドにアクセスする際のN+1クエリを防止できます。

SimpleListFilterによるカスタムlist_filter

list_filterは、変更リストページのサイドバーフィルターを有効にします。基本的なフィルタリングにはフィールド名が機能しますが、複数のフィールドにまたがる複雑なフィルタリングロジックやカスタムクエリが必要な場合はSimpleListFilterを使用します。

python
# admin.py
from datetime import date, timedelta
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Article


class PublishDateFilter(admin.SimpleListFilter):
    title = _("公開期間")
    parameter_name = "published"  # URLパラメータ: ?published=this_week

    def lookups(self, request, model_admin):
        # フィルターサイドバー用の(値, ラベル)タプルを返す
        return [
            ("this_week", _("今週")),
            ("this_month", _("今月")),
            ("this_year", _("今年")),
            ("unpublished", _("未公開")),
        ]

    def queryset(self, request, queryset):
        # self.value()に基づいてクエリセットをフィルタリング
        today = date.today()
        if self.value() == "this_week":
            return queryset.filter(publish_date__gte=today - timedelta(days=7))
        if self.value() == "this_month":
            return queryset.filter(
                publish_date__year=today.year, publish_date__month=today.month
            )
        if self.value() == "this_year":
            return queryset.filter(publish_date__year=today.year)
        if self.value() == "unpublished":
            return queryset.filter(publish_date__isnull=True)
        return queryset


class ArticleAdmin(admin.ModelAdmin):
    list_filter = [
        "status",
        PublishDateFilter,
        ("author", admin.RelatedOnlyFieldListFilter),  # 記事を持つ著者のみ表示
    ]

RelatedOnlyFieldListFilterは、データベース内のすべてのユーザーをリストするのではなく、実際に記事を持つ著者のみに著者ドロップダウンを制限します。これにより、関連モデルに多くのレコードがある場合のユーザビリティが向上します。

Django 6のファセット:サイドバーのフィルターカウント

Django 6では、各フィルターオプションの横に一致するオブジェクトの数を表示するファセットカウントが導入されました。show_facets属性でこの動作を制御します。

python
# admin.py
from django.contrib import admin
from django.contrib.admin import ShowFacets
from .models import Article


class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "status", "category"]
    list_filter = ["status", "category", "author"]
    show_facets = ShowFacets.ALWAYS  # 常にカウントを表示

    # 代替値:
    # ShowFacets.ALLOW - URLに?_facets=1がある場合にカウントを表示
    # ShowFacets.NEVER - カウントを表示しない(大規模データセット用)

ファセットカウントは、各フィルターオプションに対して追加のCOUNTクエリを実行します。数百万行のテーブルでは、パフォーマンス低下を避けるためにshow_facets = ShowFacets.NEVERを設定してください。カウントはフィルターが適用されると動的に更新され、現在のフィルター組み合わせに一致するレコード数を表示します。

一括操作のためのカスタムAdminアクション

Adminアクションは、選択されたオブジェクトに対して一括操作を実行します。@admin.actionデコレータで権限とドロップダウンに表示される説明を設定します。

python
# admin.py
from django.contrib import admin
from django.contrib import messages
from django.utils.translation import ngettext
from .models import Article


@admin.action(description="選択した記事を公開", permissions=["change"])
def publish_articles(modeladmin, request, queryset):
    # すでに公開済みの記事を除外
    unpublished = queryset.exclude(status="published")
    updated = unpublished.update(status="published")

    modeladmin.message_user(
        request,
        ngettext(
            "%d件の記事を公開しました。",
            "%d件の記事を公開しました。",
            updated,
        )
        % updated,
        messages.SUCCESS,
    )


@admin.action(description="選択した記事をJSONでエクスポート")
def export_as_json(modeladmin, request, queryset):
    from django.http import HttpResponse
    from django.core import serializers

    response = HttpResponse(content_type="application/json")
    response["Content-Disposition"] = 'attachment; filename="articles.json"'
    serializers.serialize("json", queryset, stream=response)
    return response  # HttpResponseを返すとファイルダウンロードが開始


class ArticleAdmin(admin.ModelAdmin):
    actions = [publish_articles, export_as_json]

    def get_actions(self, request):
        # ユーザー権限に基づいてアクションを条件付きで削除
        actions = super().get_actions(request)
        if not request.user.has_perm("articles.delete_article"):
            del actions["delete_selected"]
        return actions

アクションからHttpResponseを返すと、ファイルダウンロードがトリガーされるか、確認ページにリダイレクトされます。permissionsパラメータは、特定のモデル権限を持つユーザーにアクションを制限します。get_actions()をオーバーライドすると、現在のリクエストに基づいてアクションを動的に表示または非表示にできます。

Djangoの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

フィールドセットとインラインモデルによるフォーム整理

fieldsetsは、追加フォームと変更フォームを折りたたみ可能なセクションに整理します。TabularInlineStackedInlineは、関連モデルを親フォームに直接埋め込み、ページ間のナビゲーションを排除します。

python
# admin.py
from django.contrib import admin
from .models import Article, ArticleImage, ArticleComment


class ArticleImageInline(admin.TabularInline):
    model = ArticleImage
    extra = 1  # 表示する空のフォーム数
    max_num = 5  # 記事ごとの最大画像数
    fields = ["image", "caption", "order"]


class ArticleCommentInline(admin.StackedInline):
    model = ArticleComment
    extra = 0
    readonly_fields = ["created_at", "author"]
    can_delete = True


class ArticleAdmin(admin.ModelAdmin):
    fieldsets = [
        (
            None,
            {
                "fields": ["title", "slug", "author"],
            },
        ),
        (
            "コンテンツ",
            {
                "fields": ["content", "excerpt"],
                "classes": ["wide"],  # 全幅フィールド
            },
        ),
        (
            "公開設定",
            {
                "fields": ["status", "publish_date", "category", "tags"],
                "classes": ["collapse"],  # デフォルトで折りたたみ
            },
        ),
        (
            "SEO",
            {
                "fields": ["meta_title", "meta_description"],
                "classes": ["collapse"],
                "description": "検索エンジン最適化フィールド",
            },
        ),
    ]
    inlines = [ArticleImageInline, ArticleCommentInline]
    prepopulated_fields = {"slug": ["title"]}  # タイトルからスラッグを自動生成
    autocomplete_fields = ["author", "category"]  # AJAXパワードセレクト
    filter_horizontal = ["tags"]  # M2M選択用ウィジェット


admin.site.register(Article, ArticleAdmin)

TabularInlineは、関連オブジェクトをコンパクトなテーブル形式で表示し、フィールドが少ないシンプルなモデルに適しています。StackedInlineは、各関連オブジェクトを別々のフォームブロックとして表示し、多くのフィールドを持つモデルに適しています。autocomplete_fieldsオプションは、デフォルトのドロップダウンをAJAX検索ウィジェットに置き換え、関連モデルに数千のレコードがある場合に不可欠です。

Adminサイトのカスタマイズ

AdminSiteをサブクラス化すると、異なるブランディング、権限、またはモデル登録を持つ複数の管理インターフェースを作成できます。Django 6では、より厳格なパスワードポリシーのためのpassword_change_formカスタマイズが追加されました。

python
# admin.py
from django.contrib import admin
from django.contrib.admin import AdminSite
from .models import Article, Category


class ContentAdminSite(AdminSite):
    site_header = "コンテンツ管理"
    site_title = "CMS"
    index_title = "ダッシュボード"

    def has_permission(self, request):
        # 'editors'グループのスタッフに制限
        return (
            request.user.is_active
            and request.user.is_staff
            and request.user.groups.filter(name="editors").exists()
        )


content_admin = ContentAdminSite(name="content_admin")
content_admin.register(Article, ArticleAdmin)
content_admin.register(Category)

# urls.py内:
# path('content-admin/', content_admin.urls),

AdminSiteインスタンスは、独自のURL名前空間、ログインページ、登録済みモデルのセットを持ちます。このパターンは、編集者がユーザー管理やシステム設定を見る必要がない、明確なユーザーロールを持つアプリケーションに適しています。

面接質問:Django Adminカスタマイズ

これらの質問は、Djangoの技術面接で頻繁に登場します。構文だけでなく、基礎となるメカニズムを理解することが、中級エンジニアとシニアエンジニアを分けるポイントです。

Q: ソートできない計算カラムをlist_displayに追加するにはどうすればよいですか?

@admin.display()デコレータは、ソートに使用するデータベースフィールドを指定するorderingパラメータを受け入れます。これがない場合、カラムヘッダーはクリックできません。複数のレコードを集計したり外部サービスを呼び出したりする計算値は、基礎となるデータベースカラムがないためソートできません。

Q: ファセットカウントのパフォーマンスへの影響は何ですか?いつ無効にすべきですか?

ファセットは、アクティブなフィルター数を乗じた各フィルターオプションごとに1つのCOUNTクエリを実行します。1000万行のテーブルで、3つのフィールドにそれぞれ5つのフィルターオプションがある場合、ページロードごとに15の追加COUNTクエリが発生します。適切なインデックスなしで10万行を超えるテーブルをフィルタリングする場合は、show_facets = ShowFacets.NEVERを設定してください。

Q: Adminアクションとカスタム管理ビューはどう違いますか?

アクションは選択されたオブジェクトのクエリセットを操作し、変更リストの選択チェックボックスと統合されます。カスタム管理ビューは、カスタムURLを介してアクセスされるスタンドアロンページで、選択されたレコードを操作しないレポート、インポート、またはダッシュボードに適しています。アクションはリクエストをブロックするため、長時間実行される操作を実行すべきではありません。

Q: デフォルトのフィルターの代わりにRelatedOnlyFieldListFilterを使用する理由は?

デフォルトのRelatedFieldListFilterは、関連テーブル内のすべてのオブジェクトをクエリします。ForeignKeyが10万レコードのUserモデルを指している場合、フィルタードロップダウンは10万のオプションをすべてロードし、ブラウザがフリーズします。RelatedOnlyFieldListFilterは、現在のモデルのレコードに存在する値のみをクエリするため、通常ははるかに小さなセットになります。

本番環境で避けるべき一般的な間違い

開発環境で機能するいくつかのパターンは、スケール時に問題を引き起こします。

python
# admin.py
from django.contrib import admin
from .models import Order


class OrderAdmin(admin.ModelAdmin):
    # 間違い: list_select_relatedなしで関連フィールドにアクセス
    # これはN+1クエリを引き起こし、リストビューの各行に1つずつ発生
    list_display = ["id", "customer_email", "total"]

    # 修正: 関連オブジェクトをプリフェッチ
    list_select_related = ["customer"]

    def customer_email(self, obj):
        return obj.customer.email

    # 間違い: インデックスのない大きなテキストカラムでのsearch_fields
    # search_fields = ["notes"]  # 大きなテーブルでは遅い

    # 修正: インデックス付きフィールドに検索を制限
    search_fields = ["id", "customer__email"]

    # 間違い: list_displayでの高コストな計算
    # def order_total(self, obj):
    #     return sum(item.price * item.qty for item in obj.items.all())  # N+1

    # 修正: get_querysetでアノテーションを使用
    def get_queryset(self, request):
        from django.db.models import Sum, F

        qs = super().get_queryset(request)
        return qs.annotate(computed_total=Sum(F("items__price") * F("items__qty")))

    @admin.display(description="合計", ordering="computed_total")
    def total(self, obj):
        return obj.computed_total

get_queryset()をオーバーライドしてアノテーションを追加すると、行ごとにデータベースにヒットするのではなく、単一のクエリで計算値を保持できます。Django Debug Toolbarでadminをプロファイリングして、本番環境に到達する前にN+1クエリを検出してください。

サードパーティAdminテーマ:Django Unfold

Django Unfoldは、Tailwind CSSで構築されたDjango 6向けの最も積極的にメンテナンスされているadminテーマです。標準のadminを置き換えるのではなく拡張するため、既存のModelAdmin設定は引き続き機能します。

python
# settings.py
INSTALLED_APPS = [
    "unfold",  # django.contrib.adminの前に配置する必要がある
    "unfold.contrib.filters",  # 高度なフィルターウィジェット
    "unfold.contrib.forms",  # 拡張フォームウィジェット
    "django.contrib.admin",
    # ...
]

Unfoldは、ダークモード、レスポンシブレイアウト、サイドバーナビゲーション、RangeNumericFilterRangeDateFilterなどの高度なフィルタータイプを追加します。HTMXとAlpine.jsの統合により、フルSPAフレームワークのオーバーヘッドなしでインタラクティビティを提供します。カスタムダッシュボードを構築せずに洗練されたadminが必要なチームにとって、Unfoldは数週間のフロントエンド作業を節約します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

Django Adminカスタマイズで覚えておくべきこと

  • @admin.display()を使用したlist_displayでカラムを制御。ソート可能な計算値にはorderingを追加
  • SimpleListFilterは、複数フィールドにまたがるカスタムフィルターロジックや日付範囲を処理
  • Django 6のファセットはフィルターカウントを表示。大規模テーブルではCOUNTクエリのオーバーヘッドを避けるためShowFacets.NEVERで無効化
  • @admin.action(permissions=[...])を使用したAdminアクションで一括操作を処理。ファイルエクスポートにはHttpResponseを返す
  • list_select_relatedget_queryset()のアノテーション付きクエリセットを使用して、リストビューでのN+1クエリを防止
  • RelatedOnlyFieldListFilterは、大規模テーブルへのForeignKeyでフィルタリングする際のブラウザフリーズを防止
  • Admin設定を対象としたDjango面接質問でこれらのパターンを練習してください
今日のチャレンジ

Django のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年9月13日 更新

共有

関連記事