Install
$ agentstack add skill-versoxbt-claude-initial-setup-django-admin-customization ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Django Admin Customization
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.
When to Use
- User registers Django models in admin
- User needs custom list views, filters, or search
- User asks about inline editing or custom actions
- User wants to customize the admin site appearance
- User needs admin-only business operations (bulk actions, exports)
Core Patterns
ModelAdmin Configuration
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(
'{}',
color,
obj.get_status_display(),
)
Inline Models
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
Custom Actions
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
Custom List Filters
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 ( 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 Site Customization
# 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),
]
Overriding Save and Permissions
@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
Anti-Patterns
- Bare
admin.site.register(Model): Always create a ModelAdmin class. Even a
minimal one with list_display and search_fields vastly improves usability.
- No
list_select_related: Admin list views trigger N+1 queries when displaying
related fields. Always set list_select_related or override get_queryset.
- Business logic in admin actions: Admin actions should call service functions,
not contain business logic directly. Keep actions thin.
- No search fields: Users will need to find records. Always configure
search_fields on models with more than a handful of records.
- Using
list_editablewithout caution: Editable list fields bypass normal form
validation. Use sparingly and only for simple status-like fields.
Quick Reference
| 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 |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: VersoXBT
- Source: VersoXBT/claude-initial-setup
- License: MIT
- Homepage: https://github.com/VersoXBT/claude-initial-setup#installation
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.