Configuration Guide
Complete reference for configuring Django Admin with AdminConfig, decorators, and utilities.
AdminConfig Reference
AdminConfig is a Pydantic model that defines the complete admin configuration.
Basic Options
from django_cfg.modules.django_admin import AdminConfig
config = AdminConfig(
model=MyModel, # Required: Django model class
# List display
list_display=["field1", "field2"], # Field names
list_display_links=["field1"], # Clickable fields (optional)
list_per_page=50, # Pagination
list_max_show_all=200, # "Show all" limit
# Filters and search
list_filter=["status", "created_at"],
search_fields=["name", "email"],
date_hierarchy="created_at",
# Ordering
ordering=["-created_at"],
# Readonly
readonly_fields=["id", "created_at", "updated_at"],
# Form options
autocomplete_fields=["user"],
raw_id_fields=["foreign_key_field"],
prepopulated_fields={"slug": ("name",)},
# Other
save_on_top=False,
save_as=False,
preserve_filters=True,
)Performance Optimization
config = AdminConfig(
model=Order,
# Auto-applied to all queries
select_related=["user", "product", "shipping_address"],
prefetch_related=["items", "items__options"],
# Database aggregations
annotations={
'total_items': Count('items'),
'total_amount': Sum('items__price'),
'avg_price': Avg('items__price'),
},
)Automatic Optimization
select_related and prefetch_related are automatically applied to all queries in the admin. No need to override get_queryset()!
Display Fields
Define specialized field configurations that auto-generate display methods:
from django_cfg.modules.django_admin import (
BadgeField, BooleanField, CurrencyField,
DateTimeField, TextField, UserField, Icons,
)
config = AdminConfig(
model=Payment,
list_display=["id", "user", "amount", "status", "created_at"],
display_fields=[
UserField(name="user", header=True, ordering="user__username"),
CurrencyField(name="amount", currency="USD", precision=2, ordering="amount"),
BadgeField(
name="status",
label_map={
"pending": "warning",
"completed": "success",
"failed": "danger",
}
),
DateTimeField(name="created_at", ordering="created_at"),
],
)Each field type automatically generates a {field_name}_display() method.
See Field Types for complete reference.
Fieldsets
Organize form fields into sections:
from django_cfg.modules.django_admin import FieldsetConfig
config = AdminConfig(
model=Payment,
fieldsets=[
FieldsetConfig(
title="Basic Information",
fields=["id", "internal_payment_id", "user", "status"]
),
FieldsetConfig(
title="Payment Details",
fields=["amount_usd", "currency", "pay_amount"]
),
FieldsetConfig(
title="Timestamps",
fields=["created_at", "updated_at"],
collapsed=True # Start collapsed
),
],
)Widgets
Configure custom widgets for specific fields using WidgetConfig classes. This is especially useful for JSON fields, text fields, and other complex inputs.
JSON Widget Configuration
from django_cfg.modules.django_admin import AdminConfig, JSONWidgetConfig
config = AdminConfig(
model=BotConfig,
# Centralized widget configuration
widgets=[
# Editable JSON field
JSONWidgetConfig(
field="settings",
mode="tree", # Interactive tree editor
height="400px",
show_copy_button=True,
),
# Read-only JSON field
JSONWidgetConfig(
field="schema",
mode="view", # Read-only display
height="500px",
show_copy_button=True,
),
],
fieldsets=[
FieldsetConfig(
title="Configuration",
fields=["settings", "schema"], # Widget config applied automatically
),
],
)JSON Widget Modes
| Mode | Use Case | Features |
|---|---|---|
tree | Complex nested JSON | Interactive tree, expand/collapse, inline editing |
code | Raw JSON editing | Text editor with syntax highlighting |
view | Display only | Read-only, formatted display, no editing |
JSON Widget Parameters
JSONWidgetConfig(
field="config_schema", # Required: field name
mode="tree", # "tree", "code", or "view"
height="400px", # Editor height
width=None, # Editor width (default: 100%)
show_copy_button=True, # Show copy button (default: True)
)Automatic JSON Widget
JSONWidget is automatically applied to all Django JSONField models:
from django.db import models
class Bot(models.Model):
settings = models.JSONField(default=dict) # Auto-gets JSON editor!
# No widget configuration needed unless you want to customizeWhen to Configure
Only add JSONWidgetConfig when you need to:
- Change the editor mode (tree/code/view)
- Adjust height for large JSON
- Disable copy button
- Set specific width
Multiple Widget Types
from django_cfg.modules.django_admin import (
AdminConfig,
JSONWidgetConfig,
TextWidgetConfig,
)
config = AdminConfig(
model=Article,
widgets=[
# JSON widget for metadata
JSONWidgetConfig(
field="metadata",
mode="tree",
height="300px",
),
# Text widget for description
TextWidgetConfig(
field="description",
placeholder="Enter article description",
rows=5,
),
],
)Widget Config Location
Always define widgets in AdminConfig.widgets, not in FieldsetConfig. Fieldsets only define field structure.
Actions
Define admin actions declaratively using ActionConfig. This provides a clean, type-safe way to define actions with enhanced UI features.
Basic Action Example
from django_cfg.modules.django_admin import ActionConfig
from django.contrib import messages
def mark_as_completed(modeladmin, request, queryset):
"""Mark items as completed."""
count = queryset.update(status='completed')
messages.success(request, f"Marked {count} items as completed")
config = AdminConfig(
model=Payment,
actions=[
ActionConfig(
name="mark_as_completed",
description="Mark as completed",
variant="success",
handler=mark_as_completed
),
],
)ActionConfig Parameters
- name (str, required): Action function name - must match the handler function name
- description (str, required): Display text shown in the admin actions dropdown
- action_type (str, optional): Type of action. Options:
"bulk"- Traditional bulk action (requires selecting items) - default"changelist"- Button above the listing (no selection required)
- variant (str, optional): Button color variant. Options:
"default"- Gray button (default)"success"- Green button for positive actions"warning"- Orange button for cautionary actions"danger"- Red button for destructive actions"primary"- Blue button for primary actions"info"- Light blue button for informational actions
- icon (str, optional): Material Icon name (e.g.,
"check_circle","warning","delete") - url_path (str, optional): Custom URL path for changelist actions (auto-generated if not provided)
- confirmation (bool, optional): If
True, shows confirmation dialog before executing (default:False) - handler (callable or str, required): Action handler function or import path to handler
- permissions (list[str], optional): Required permissions to show this action
Action Handler Signatures
- Bulk actions:
handler(modeladmin, request, queryset)- receives selected items - Changelist actions:
handler(modeladmin, request)- no queryset, must return HttpResponse
Multiple Actions with Different Variants
from django_cfg.modules.django_admin import ActionConfig
config = AdminConfig(
model=Lead,
actions=[
ActionConfig(
name="mark_as_contacted",
description="Mark as contacted",
variant="warning",
handler=mark_as_contacted
),
ActionConfig(
name="mark_as_qualified",
description="Mark as qualified",
variant="primary",
handler=mark_as_qualified
),
ActionConfig(
name="mark_as_converted",
description="Mark as converted",
variant="success",
handler=mark_as_converted
),
ActionConfig(
name="mark_as_rejected",
description="Mark as rejected",
variant="danger",
handler=mark_as_rejected
),
],
)Actions with Icons and Confirmation
config = AdminConfig(
model=Document,
actions=[
ActionConfig(
name="publish_documents",
description="Publish selected documents",
variant="success",
icon="publish",
confirmation=True, # Shows confirmation dialog
handler=publish_documents
),
ActionConfig(
name="archive_documents",
description="Archive selected documents",
variant="warning",
icon="archive",
confirmation=True,
handler=archive_documents
),
ActionConfig(
name="delete_permanently",
description="Delete permanently",
variant="danger",
icon="delete_forever",
confirmation=True, # Important for destructive actions!
handler=delete_permanently
),
],
)Organizing Actions in Separate Files
For cleaner code organization, define action handlers in a separate actions.py file:
# leads/admin/actions.py
from django.contrib import messages
def mark_as_contacted(modeladmin, request, queryset):
"""Mark selected leads as contacted."""
updated = queryset.update(status='contacted')
messages.success(request, f"Marked {updated} leads as contacted.")
def mark_as_qualified(modeladmin, request, queryset):
"""Mark selected leads as qualified."""
updated = queryset.update(status='qualified')
messages.success(request, f"Marked {updated} leads as qualified.")
def mark_as_converted(modeladmin, request, queryset):
"""Mark selected leads as converted."""
updated = queryset.update(status='converted')
messages.success(request, f"Marked {updated} leads as converted.")
def mark_as_rejected(modeladmin, request, queryset):
"""Mark selected leads as rejected."""
updated = queryset.update(status='rejected')
messages.warning(request, f"Marked {updated} leads as rejected.")# leads/admin/leads_admin.py
from django_cfg.modules.django_admin import AdminConfig, ActionConfig
from .actions import mark_as_contacted, mark_as_qualified, mark_as_converted, mark_as_rejected
config = AdminConfig(
model=Lead,
actions=[
ActionConfig(
name="mark_as_contacted",
description="Mark as contacted",
variant="warning",
handler=mark_as_contacted
),
ActionConfig(
name="mark_as_qualified",
description="Mark as qualified",
variant="primary",
handler=mark_as_qualified
),
ActionConfig(
name="mark_as_converted",
description="Mark as converted",
variant="success",
handler=mark_as_converted
),
ActionConfig(
name="mark_as_rejected",
description="Mark as rejected",
variant="danger",
handler=mark_as_rejected
),
],
)Actions with Permissions
Restrict actions to users with specific permissions:
config = AdminConfig(
model=Payment,
actions=[
ActionConfig(
name="approve_payment",
description="Approve payment",
variant="success",
icon="check_circle",
permissions=["payments.approve_payment"], # Custom permission
handler=approve_payment
),
ActionConfig(
name="reject_payment",
description="Reject payment",
variant="danger",
icon="cancel",
permissions=["payments.reject_payment"],
confirmation=True,
handler=reject_payment
),
],
)Best Practices
-
Use appropriate variants: Match button colors to action semantics
- Green (
success) for positive actions (approve, complete, publish) - Red (
danger) for destructive actions (delete, reject, cancel) - Orange (
warning) for cautionary actions (archive, suspend) - Blue (
primary) for primary workflow actions
- Green (
-
Always use confirmation for destructive actions: Set
confirmation=Truefor actions that delete or permanently modify data -
Organize actions in separate files: Keep action handlers in
actions.pyfor better code organization -
Use meaningful icons: Choose icons that clearly represent the action
-
Add proper permissions: Restrict sensitive actions using the
permissionsparameter -
Provide user feedback: Always use Django messages to inform users about action results:
from django.contrib import messages def my_action(modeladmin, request, queryset): count = queryset.update(...) messages.success(request, f"Updated {count} items")
Changelist Actions (Buttons Above Listing)
Changelist actions are buttons that appear above the listing and don’t require selecting items. They’re perfect for global operations like imports, exports, synchronizations, or bulk operations.
Key Differences from Bulk Actions:
| Feature | Bulk Actions | Changelist Actions |
|---|---|---|
| Location | Dropdown menu | Buttons above listing |
| Selection Required | ✅ Yes | ❌ No |
| Handler Signature | (modeladmin, request, queryset) | (modeladmin, request) |
| Return Value | None | HttpResponse (redirect) |
action_type | "bulk" (default) | "changelist" |
Example: Sync Buttons for External API
# proxies/admin/actions.py
from django.shortcuts import redirect
from django.urls import reverse
from django.contrib import messages
from django.core.management import call_command
from io import StringIO
def sync_proxy6(modeladmin, request):
"""Synchronize proxies from Proxy6 provider."""
try:
messages.info(request, "🔄 Syncing proxies from Proxy6...")
# Call management command
out = StringIO()
call_command('sync_proxy_providers', provider='proxy6', stdout=out)
# Show command output
output = out.getvalue()
if output:
messages.success(request, output)
except Exception as e:
messages.error(request, f"❌ Failed to sync Proxy6: {str(e)}")
# IMPORTANT: Must return redirect for changelist actions
return redirect(reverse('admin:proxies_proxy_changelist'))
def sync_all_providers(modeladmin, request):
"""Synchronize proxies from all providers."""
try:
messages.info(request, "🔄 Syncing all providers...")
out = StringIO()
call_command('sync_proxy_providers', provider='all', stdout=out)
output = out.getvalue()
if output:
messages.success(request, output)
except Exception as e:
messages.error(request, f"❌ Failed to sync: {str(e)}")
return redirect(reverse('admin:proxies_proxy_changelist'))# proxies/admin/proxy_admin.py
from django_cfg.modules.django_admin import AdminConfig, ActionConfig
config = AdminConfig(
model=Proxy,
actions=[
# Bulk actions (require selection)
ActionConfig(
name='test_selected_proxies',
action_type='bulk', # Default
description='Test selected proxies',
variant='warning',
icon='speed',
confirmation=True,
handler='apps.proxies.admin.actions.test_selected_proxies',
),
# Changelist actions (buttons above listing, no selection needed)
ActionConfig(
name='sync_all_providers',
action_type='changelist', # 🎯 Button above listing!
description='🔄 Sync All Providers',
variant='primary',
icon='sync',
confirmation=True,
handler='apps.proxies.admin.actions.sync_all_providers',
),
ActionConfig(
name='sync_proxy6',
action_type='changelist', # 🎯 Button above listing!
description='Sync Proxy6',
variant='info',
icon='cloud_sync',
handler='apps.proxies.admin.actions.sync_proxy6',
),
],
)Result in Admin:
┌────────────────────────────────────────────────────┐
│ [🔄 Sync All Providers] [Sync Proxy6] │ ← Changelist actions
├────────────────────────────────────────────────────┤
│ Actions: [▼ Test selected proxies] │ ← Bulk actions dropdown
│ │
│ ☐ ID Host Provider Status │
│ ☐ abc 1.2.3.4 proxy6 Active │
└────────────────────────────────────────────────────┘Common Use Cases for Changelist Actions:
- Data Synchronization: Sync with external APIs, refresh data
- Bulk Imports: Import data from files without selecting items
- Reports: Generate reports for all items
- Cache Operations: Clear caches, rebuild indexes
- Maintenance: Run cleanup tasks, optimize database
Important for Changelist Actions
- Must return HttpResponse: Always return
redirect()or other HttpResponse - No queryset parameter: Handler receives only
(modeladmin, request) - Use for model-level operations: Not for selected items
- Always provide user feedback: Use Django messages framework
Old-Style Actions (Not Recommended)
Do NOT use old-style actions with .short_description attributes:
# ❌ BAD - Old-style action
def my_action(self, request, queryset):
# do stuff
pass
my_action.short_description = "My Action"
# ✅ GOOD - Use ActionConfig instead
ActionConfig(
name="my_action",
description="My Action",
variant="primary",
handler=my_action
)Old-style actions lack support for variants, icons, confirmation, and permissions. Always use ActionConfig for declarative, feature-rich actions.
Import/Export
Enable import/export functionality:
from import_export import resources
class PaymentResource(resources.ModelResource):
class Meta:
model = Payment
fields = ('id', 'internal_payment_id', 'amount_usd', 'status')
config = AdminConfig(
model=Payment,
import_export_enabled=True,
resource_class=PaymentResource,
)Decorators
Django Admin provides decorators for custom display methods.
@computed_field
For custom display logic in list views:
from django_cfg.modules.django_admin import computed_field, Icons
@admin.register(User)
class UserAdmin(PydanticAdmin):
config = user_config
@computed_field("Full Name", ordering="last_name")
def full_name(self, obj):
"""Display full name with badge."""
name = obj.get_full_name()
if not name:
return self.html.badge("No name", variant="secondary", icon=Icons.PERSON)
return self.html.badge(name, variant="primary", icon=Icons.PERSON)
@computed_field("Status", boolean=False)
def status_display(self, obj):
"""Status with conditional colors."""
if obj.is_superuser:
return self.html.badge("Superuser", variant="danger", icon=Icons.ADMIN_PANEL_SETTINGS)
elif obj.is_staff:
return self.html.badge("Staff", variant="warning", icon=Icons.SETTINGS)
elif obj.is_active:
return self.html.badge("Active", variant="success", icon=Icons.CHECK_CIRCLE)
else:
return self.html.badge("Inactive", variant="secondary", icon=Icons.CANCEL)Parameters:
short_description(str): Column header textordering(str, optional): Field name for sortingboolean(bool): Display as boolean iconempty_value(str): Value when None/empty (default: ”—”)
@annotated_field
For values computed from queryset annotations:
from django.db.models import Count
from django_cfg.modules.django_admin import annotated_field
config = AdminConfig(
model=User,
annotations={
'transaction_count': Count('transactions'),
},
)
@admin.register(User)
class UserAdmin(PydanticAdmin):
config = config
@annotated_field("Transactions", annotation_name="transaction_count")
def transaction_display(self, obj):
"""Display transaction count from annotation."""
count = getattr(obj, 'transaction_count', 0)
if count == 0:
return self.html.empty()
return self.html.badge(f"{count} transactions", variant="info", icon=Icons.RECEIPT)Parameters:
short_description(str): Column header textannotation_name(str): Name of the annotation in querysetordering(str, optional): Field name for sorting (defaults to annotation_name)empty_value(str): Value when None/empty
HTML Builder Utilities
Access via self.html in any admin method for building rich HTML displays.
badge()
Colored badge with optional icon:
def status_display(self, obj):
return self.html.badge(
"Active",
variant="success", # primary, success, warning, danger, info, secondary
icon=Icons.CHECK_CIRCLE
)Variants:
primary- Blue (default)success- Greenwarning- Yellow/Orangedanger- Redinfo- Light bluesecondary- Gray
span()
Wrapped text with CSS classes:
def custom_display(self, obj):
return self.html.span("Important", "font-semibold text-red-600")inline()
Join multiple items horizontally:
def details_display(self, obj):
return self.html.inline([
self.html.span("ID:", "font-semibold"),
self.html.span(obj.id, ""),
self.html.badge(obj.status, variant="info"),
], separator=" | ")Parameters:
items(list): List of HTML elementsseparator(str): Separator between items (default: ” | “)
icon()
Material icon only:
def has_email_icon(self, obj):
if obj.email:
return self.html.icon(Icons.CHECK_CIRCLE, size="sm")
return self.html.icon(Icons.CANCEL, size="sm")Sizes: xs, sm, base, lg, xl
icon_text()
Icon + text combination:
def stats_display(self, obj):
return self.html.inline([
self.html.icon_text(Icons.EDIT, obj.posts_count),
self.html.icon_text(Icons.CHAT, obj.comments_count),
])link()
Clickable link:
def external_link(self, obj):
return self.html.link(
obj.website_url,
"Visit Website",
css_class="text-blue-600",
target="_blank"
)empty()
Empty placeholder:
def optional_field(self, obj):
if not obj.description:
return self.html.empty() # Shows "—"
return obj.descriptionDisplay Utilities
Advanced display utilities for specialized formatting.
UserDisplay
Utility for user display with avatar support:
from django_cfg.modules.django_admin.utils.displays import UserDisplay
from django_cfg.modules.django_admin.models.display_models import UserDisplayConfig
def user_display(self, obj):
"""User with avatar."""
config = UserDisplayConfig(
show_avatar=True,
show_email=True,
avatar_size="md"
)
return UserDisplay.with_avatar(obj.user, config)
def simple_user(self, obj):
"""Simple user display without avatar."""
return UserDisplay.simple(obj.user)UserDisplayConfig Parameters:
show_avatar(bool): Show avatar imageshow_email(bool): Show email below nameavatar_size(str): Avatar size - sm, md, lg
MoneyDisplay
Utility for currency formatting:
from django_cfg.modules.django_admin.utils.displays import MoneyDisplay
from django_cfg.modules.django_admin.models.display_models import MoneyDisplayConfig
def amount_display(self, obj):
"""Smart currency formatting."""
config = MoneyDisplayConfig(
currency="USD",
decimal_places=2,
thousand_separator=True,
show_currency_symbol=True,
show_sign=False,
smart_decimal_places=False
)
return MoneyDisplay.amount(obj.amount, config)
def rate_display(self, obj):
"""Exchange rate display."""
config = MoneyDisplayConfig(
currency="USD",
rate_mode=True, # Special formatting for rates
smart_decimal_places=True # Auto-adjust decimals
)
return MoneyDisplay.amount(obj.exchange_rate, config)MoneyDisplayConfig Parameters:
currency(str): Currency code (USD, EUR, BTC, etc.)decimal_places(int): Number of decimal places (default: 2)thousand_separator(bool): Add thousand separatorsshow_currency_symbol(bool): Show currency symbolshow_sign(bool): Show + for positive amountssmart_decimal_places(bool): Auto-adjust decimals based on amountrate_mode(bool): Special formatting for exchange rates
DateTimeDisplay
Utility for datetime formatting:
from django_cfg.modules.django_admin.utils.displays import DateTimeDisplay
from django_cfg.modules.django_admin.models.display_models import DateTimeDisplayConfig
def created_display(self, obj):
"""Datetime with relative time in local timezone."""
config = DateTimeDisplayConfig(
datetime_format="%Y-%m-%d %H:%M:%S",
show_relative=True,
use_local_tz=True, # Convert to local timezone
)
return DateTimeDisplay.relative(obj.created_at, config)
def server_time_display(self, obj):
"""Datetime in UTC (no conversion)."""
config = DateTimeDisplayConfig(
show_relative=False,
use_local_tz=False, # Keep UTC
)
return DateTimeDisplay.relative(obj.server_timestamp, config)
def compact_time(self, obj):
"""Compact datetime display."""
return DateTimeDisplay.compact(obj.updated_at)DateTimeDisplayConfig Parameters:
datetime_format(str): strftime format string (default:"%Y-%m-%d %H:%M:%S")show_relative(bool): Show “2 hours ago” below absolute time (default:True)use_local_tz(bool): Convert to local timezone (default:True)
Timezone Resolution:
- First tries
DjangoConfig.admin_timezoneif configured - Falls back to system timezone via
tzlocallibrary
Complete Example
Putting it all together:
from django.contrib import admin
from django.db.models import Count
from django_cfg.modules.django_admin import (
AdminConfig, BadgeField, CurrencyField, DateTimeField,
FieldsetConfig, Icons, UserField, computed_field, annotated_field,
)
from django_cfg.modules.django_admin.base import PydanticAdmin
from django_cfg.modules.django_admin.utils.displays import MoneyDisplay
# Configuration
payment_config = AdminConfig(
model=Payment,
# Performance
select_related=["user", "currency"],
prefetch_related=["transactions"],
annotations={
'transaction_count': Count('transactions'),
},
# Display
list_display=["internal_payment_id", "user", "amount_usd", "status", "transaction_count_display", "created_at"],
display_fields=[
BadgeField(name="internal_payment_id", variant="info", icon=Icons.RECEIPT),
UserField(name="user", header=True, ordering="user__username"),
CurrencyField(name="amount_usd", currency="USD", precision=2, ordering="amount_usd"),
BadgeField(
name="status",
label_map={
"pending": "warning",
"completed": "success",
"failed": "danger",
}
),
DateTimeField(name="created_at", ordering="created_at"),
],
# Fieldsets
fieldsets=[
FieldsetConfig(title="Basic", fields=["id", "internal_payment_id", "user", "status"]),
FieldsetConfig(title="Payment", fields=["amount_usd", "currency", "pay_amount"]),
FieldsetConfig(title="Details", fields=["payment_details_display"], collapsed=True),
],
readonly_fields=["payment_details_display"],
list_filter=["status", "created_at"],
search_fields=["internal_payment_id", "user__username"],
)
@admin.register(Payment)
class PaymentAdmin(PydanticAdmin):
config = payment_config
# Computed field with decorator
@annotated_field("Transactions", annotation_name="transaction_count")
def transaction_count_display(self, obj):
"""Display transaction count from annotation."""
count = getattr(obj, 'transaction_count', 0)
if count == 0:
return self.html.empty()
return self.html.badge(f"{count} txns", variant="info", icon=Icons.RECEIPT)
# Custom readonly field using self.html
def payment_details_display(self, obj):
"""Detailed payment information."""
details = []
# Basic info
details.append(self.html.inline([
self.html.span("Internal ID:", "font-semibold"),
self.html.span(obj.internal_payment_id, "")
], separator=" "))
# Transaction link
if obj.transaction_hash:
details.append(self.html.inline([
self.html.span("Transaction:", "font-semibold"),
self.html.link(
obj.get_explorer_link(),
obj.transaction_hash[:16] + "...",
target="_blank"
)
], separator=" "))
# Confirmations badge
if obj.confirmations_count > 0:
details.append(self.html.badge(
f"{obj.confirmations_count} confirmations",
variant="info",
icon=Icons.CHECK_CIRCLE
))
# Pay amount with MoneyDisplay utility
if obj.pay_amount:
from django_cfg.modules.django_admin.utils.displays import MoneyDisplay
from django_cfg.modules.django_admin.models.display_models import MoneyDisplayConfig
config = MoneyDisplayConfig(currency=obj.currency.token, smart_decimal_places=True)
amount_html = MoneyDisplay.amount(obj.pay_amount, config)
details.append(self.html.inline([
self.html.span("Pay Amount:", "font-semibold"),
amount_html
], separator=" "))
return "<br>".join(details)
payment_details_display.short_description = "Payment Details"Best Practices
1. Use Declarative Fields When Possible
# ✅ Good - Auto-generated display method
display_fields=[
CurrencyField(name="amount", currency="USD", precision=2)
]
# ⚠️ Only when needed - Manual method
@computed_field("Amount")
def amount_display(self, obj):
return f"${obj.amount:.2f}"2. Use self.html Builder
# ✅ Good - Rich HTML builder
def status(self, obj):
return self.html.badge("Active", variant="success", icon=Icons.CHECK_CIRCLE)
def details(self, obj):
return self.html.inline([
self.html.span("ID:", "font-semibold"),
self.html.badge(obj.id, variant="info"),
], separator=" ")3. Apply Query Optimizations in Config
# ✅ Good - Automatic optimization
config = AdminConfig(
model=Order,
select_related=["user", "product"],
prefetch_related=["items"],
)
# These optimizations are auto-applied to all queries4. Use Annotations for Database Aggregations
# ✅ Database-level computation (efficient)
config = AdminConfig(
model=User,
annotations={'order_count': Count('orders')},
)
@annotated_field("Orders", annotation_name="order_count")
def order_count_display(self, obj):
return self.html.badge(f"{obj.order_count} orders", variant="info")
# ⚠️ Python-level computation (creates N+1 queries)
@computed_field("Orders")
def order_count_display(self, obj):
count = obj.orders.count() # Extra query per row
return f"{count} orders"Theme & Styling Enhancements
Fieldset Headers
Fieldset headers automatically adjust their appearance based on state:
Collapsed State:
- Subtle background color for minimal visual weight
- Standard text color
Open State:
- Primary color background
- White text for high contrast
- Primary color border around content
This provides clear visual feedback about which sections are currently expanded.
JSON Field Display
JSON fields in readonly mode now feature:
Syntax Highlighting:
- Uses Pygments library for professional code highlighting
- Different colors for keys, strings, numbers, booleans, and null values
- Automatic theme switching (light/dark)
Unicode Support:
- Proper display of non-ASCII characters (Cyrillic, Chinese, emoji, etc.)
- No more escaped Unicode sequences like
\u043a - JSON formatted with
ensure_ascii=False
Theme-Aware Colors:
- Light theme:
colorfulPygments style - Dark theme:
monokaiPygments style - Automatic switching with Unfold dark mode
Example:
# Your JSON data
{
"status": "активен", # Displays correctly, not as \u0430\u043a\u0442\u0438\u0432\u0435\u043d
"color": "красный",
"emoji": "🎨"
}JSON Display Features
- Automatic syntax highlighting for all readonly JSON fields
- Copy button for easy JSON copying
- Proper Unicode support for international characters
- Theme-aware color schemes
- No configuration needed - works automatically!
Custom Theme Styles
The admin includes custom CSS in static/admin/css/theme.css for:
- Fieldset header states (collapsed/open)
- Border styling for open fieldsets
- Theme-compatible card designs
- Icon spacing and alignment
Next Steps
- Examples - Real-world working examples
- Field Types - Complete field reference
- Filters - Complete guide to filters
- Quick Start - Get started guide