From claude-initial-setup
Customize Django admin: ModelAdmin, inline models, custom actions, list_display, list_filter, search, admin site customization, and custom views. Use when the user configures Django admin, asks about admin customization, registers models, or needs admin-side data management features. Trigger when you see bare admin.site.register() calls without ModelAdmin classes.
npx claudepluginhub versoxbt/claude-initial-setup --plugin claude-initial-setupThis skill uses the workspace's default tool permissions.
Build powerful admin interfaces with Django's built-in admin. A well-configured admin
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Build powerful admin interfaces with Django's built-in admin. A well-configured admin panel eliminates the need for custom CRUD dashboards and gives non-technical users a safe way to manage data.
from django.contrib import admin
from django.utils.html import format_html
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
# List view configuration
list_display = [
"title", "author_name", "category", "status",
"view_count", "colored_status", "created_at",
]
list_display_links = ["title"]
list_editable = ["status", "category"]
list_filter = ["status", "category", "created_at"]
list_per_page = 25
list_select_related = ["author", "category"]
# Search
search_fields = ["title", "body", "author__username", "author__email"]
search_help_text = "Search by title, body, or author"
# Detail view
readonly_fields = ["slug", "view_count", "created_at", "updated_at"]
prepopulated_fields = {"slug": ("title",)}
autocomplete_fields = ["author", "category"]
filter_horizontal = ["tags"]
# Fieldsets -- organize detail view into sections
fieldsets = [
(None, {
"fields": ["title", "slug", "body"],
}),
("Classification", {
"fields": ["category", "tags", "status"],
}),
("Metadata", {
"classes": ["collapse"], # Collapsible section
"fields": ["author", "view_count", "created_at", "updated_at"],
}),
]
# Date hierarchy for drill-down navigation
date_hierarchy = "created_at"
# Ordering
ordering = ["-created_at"]
@admin.display(description="Author", ordering="author__last_name")
def author_name(self, obj):
return obj.author.get_full_name()
@admin.display(description="Status")
def colored_status(self, obj):
colors = {"draft": "gray", "published": "green", "archived": "red"}
color = colors.get(obj.status, "black")
return format_html(
'<span style="color: {};">{}</span>',
color,
obj.get_status_display(),
)
Edit related objects directly on the parent's admin page.
class CommentInline(admin.TabularInline):
model = Comment
extra = 0 # No empty forms by default
readonly_fields = ["author", "created_at"]
fields = ["author", "text", "is_approved", "created_at"]
def has_add_permission(self, request, obj=None):
return False # Comments are created by users, not admins
class ImageInline(admin.StackedInline):
model = ArticleImage
extra = 1
max_num = 10
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
inlines = [ImageInline, CommentInline]
list_display = ["title", "status", "comment_count"]
def get_queryset(self, request):
return (
super().get_queryset(request)
.select_related("author", "category")
.annotate(comment_count=Count("comments"))
)
@admin.display(description="Comments", ordering="comment_count")
def comment_count(self, obj):
return obj.comment_count
Bulk operations on selected objects from the list view.
from django.contrib import messages
from django.http import HttpResponse
import csv
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
actions = ["publish_selected", "archive_selected", "export_as_csv"]
@admin.action(description="Publish selected articles")
def publish_selected(self, request, queryset):
updated = queryset.filter(status="draft").update(status="published")
self.message_user(
request,
f"{updated} article(s) published.",
messages.SUCCESS,
)
@admin.action(description="Archive selected articles")
def archive_selected(self, request, queryset):
updated = queryset.exclude(status="archived").update(status="archived")
self.message_user(request, f"{updated} article(s) archived.")
@admin.action(description="Export selected as CSV")
def export_as_csv(self, request, queryset):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="articles.csv"'
writer = csv.writer(response)
writer.writerow(["Title", "Author", "Status", "Views", "Created"])
for article in queryset.select_related("author"):
writer.writerow([
article.title,
article.author.get_full_name(),
article.status,
article.view_count,
article.created_at.isoformat(),
])
return response
from django.utils import timezone
from datetime import timedelta
class ViewCountRangeFilter(admin.SimpleListFilter):
title = "view count"
parameter_name = "views"
def lookups(self, request, model_admin):
return [
("low", "Low (< 100)"),
("medium", "Medium (100-1000)"),
("high", "High (> 1000)"),
]
def queryset(self, request, queryset):
if self.value() == "low":
return queryset.filter(view_count__lt=100)
if self.value() == "medium":
return queryset.filter(view_count__gte=100, view_count__lte=1000)
if self.value() == "high":
return queryset.filter(view_count__gt=1000)
return queryset
class RecentFilter(admin.SimpleListFilter):
title = "recency"
parameter_name = "recent"
def lookups(self, request, model_admin):
return [
("today", "Today"),
("week", "This week"),
("month", "This month"),
]
def queryset(self, request, queryset):
now = timezone.now()
ranges = {
"today": now - timedelta(days=1),
"week": now - timedelta(weeks=1),
"month": now - timedelta(days=30),
}
if self.value() in ranges:
return queryset.filter(created_at__gte=ranges[self.value()])
return queryset
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_filter = ["status", "category", ViewCountRangeFilter, RecentFilter]
# admin.py -- customize the default admin site
admin.site.site_header = "My Project Admin"
admin.site.site_title = "My Project"
admin.site.index_title = "Dashboard"
# Custom admin site for multi-tenant or separate admin panels
class AnalyticsAdminSite(admin.AdminSite):
site_header = "Analytics Dashboard"
site_title = "Analytics"
def has_permission(self, request):
return request.user.is_active and request.user.groups.filter(
name="analytics"
).exists()
analytics_admin = AnalyticsAdminSite(name="analytics_admin")
analytics_admin.register(PageView, PageViewAdmin)
# urls.py
urlpatterns = [
path("admin/", admin.site.urls),
path("analytics/", analytics_admin.urls),
]
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if not change: # Creating new object
obj.author = request.user
super().save_model(request, obj, form, change)
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
def has_change_permission(self, request, obj=None):
if obj is 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 get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser:
return self.readonly_fields + ["status", "author"]
return self.readonly_fields
admin.site.register(Model): Always create a ModelAdmin class. Even a
minimal one with list_display and search_fields vastly improves usability.list_select_related: Admin list views trigger N+1 queries when displaying
related fields. Always set list_select_related or override get_queryset.search_fields on models with more than a handful of records.list_editable without caution: Editable list fields bypass normal form
validation. Use sparingly and only for simple status-like fields.| Feature | Configuration |
|---|---|
| Columns | list_display = ["field1", "field2"] |
| Editable in list | list_editable = ["status"] |
| Sidebar filters | list_filter = ["status", CustomFilter] |
| Search | search_fields = ["title", "author__name"] |
| Auto-slug | prepopulated_fields = {"slug": ("title",)} |
| Sections | fieldsets = [(name, {"fields": [...]})] |
| Inline editing | inlines = [CommentInline] |
| Bulk actions | actions = ["my_action"] |
| Date drill-down | date_hierarchy = "created_at" |
| FK autocomplete | autocomplete_fields = ["author"] |
| M2M widget | filter_horizontal = ["tags"] |
| Per-page count | list_per_page = 25 |