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

Django Adminのカスタマイズは、標準装備の管理画面を本番環境向けの実用的なバックオフィスツールへと変貌させます。Django 6では、フィルターのファセットカウント、カスタマイズ可能なパスワード変更フォーム、Font Awesomeアイコンなど、管理画面の機能が大幅に強化されました。適切に設定されたAdmin画面は、日常業務において非常に使いやすいものとなります。
ModelAdminはすべてのAdminカスタマイズの中核となるクラスです。list_display、list_filter、search_fields、fieldsets、カスタムアクションをマスターすれば、Admin設定の90%に対応できます。
ModelAdminの基本:list_displayとsearch_fields
list_displayは、変更リストページに表示されるカラムを制御します。単純なフィールド名だけでなく、計算値を表示するために@admin.display()デコレータで装飾された呼び出し可能オブジェクトも受け入れます。search_fieldsは、デフォルトでicontainsを使用してレコードをフィルタリングする検索ボックスを有効にします。
# 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を使用します。
# 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属性でこの動作を制御します。
# 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デコレータで権限とドロップダウンに表示される説明を設定します。
# 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は、追加フォームと変更フォームを折りたたみ可能なセクションに整理します。TabularInlineとStackedInlineは、関連モデルを親フォームに直接埋め込み、ページ間のナビゲーションを排除します。
# 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カスタマイズが追加されました。
# 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は、現在のモデルのレコードに存在する値のみをクエリするため、通常ははるかに小さなセットになります。
本番環境で避けるべき一般的な間違い
開発環境で機能するいくつかのパターンは、スケール時に問題を引き起こします。
# 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_totalget_queryset()をオーバーライドしてアノテーションを追加すると、行ごとにデータベースにヒットするのではなく、単一のクエリで計算値を保持できます。Django Debug Toolbarでadminをプロファイリングして、本番環境に到達する前にN+1クエリを検出してください。
サードパーティAdminテーマ:Django Unfold
Django Unfoldは、Tailwind CSSで構築されたDjango 6向けの最も積極的にメンテナンスされているadminテーマです。標準のadminを置き換えるのではなく拡張するため、既存のModelAdmin設定は引き続き機能します。
# settings.py
INSTALLED_APPS = [
"unfold", # django.contrib.adminの前に配置する必要がある
"unfold.contrib.filters", # 高度なフィルターウィジェット
"unfold.contrib.forms", # 拡張フォームウィジェット
"django.contrib.admin",
# ...
]Unfoldは、ダークモード、レスポンシブレイアウト、サイドバーナビゲーション、RangeNumericFilterやRangeDateFilterなどの高度なフィルタータイプを追加します。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_relatedとget_queryset()のアノテーション付きクエリセットを使用して、リストビューでのN+1クエリを防止RelatedOnlyFieldListFilterは、大規模テーブルへのForeignKeyでフィルタリングする際のブラウザフリーズを防止- Admin設定を対象としたDjango面接質問でこれらのパターンを練習してください
Django のバグを見つけられますか
実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

執筆
Anthony Fillion-MailletSharpSkill 創業者
10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。
2026年9月13日 更新
共有
関連記事

Django ミドルウェア完全ガイド 2026:カスタムミドルウェア、ロギング実装と面接対策
Django 6.0のミドルウェアを徹底解説。カスタムミドルウェアの作成方法、非同期ミドルウェア、リクエストIDロギング、そして技術面接で頻出の質問と回答を詳しく説明します。

Django SignalsとCelery Tasks 2026年版: 適切な使い分けと面接対策ガイド
Django SignalsとCeleryタスクの違いを徹底解説。同期処理と非同期処理の使い分け、パフォーマンス最適化、面接でよく聞かれる質問と回答例を網羅的に紹介します。

Django Channels 2026年版完全ガイド:WebSocket、リアルタイム通信、面接対策
Django Channels 4.xでWebSocketコンシューマー、Redisチャンネルレイヤー、面接頻出質問を習得します。Uvicornを使用した本番環境対応のASGIデプロイメントコード例を含む実践的チュートリアルです。