Real-World Examples
Complete working examples from production code showing best practices and common patterns.
Payment Admin
Complete payment management with transaction details, QR codes, and status tracking.
"""
Payment Admin - Complete example from production
Features: Badges, Currency, DateTime, Custom readonly fields with self.html
"""
from django.contrib import admin
from django.utils import timezone
from django_cfg.modules.django_admin import (
AdminConfig, BadgeField, CurrencyField, DateTimeField,
FieldsetConfig, Icons, UserField,
)
from django_cfg.modules.django_admin.base import PydanticAdmin
# Declarative configuration
payment_config = AdminConfig(
model=Payment,
# Performance optimization
select_related=["user", "currency"],
# List display
list_display=[
"internal_payment_id",
"user",
"amount_usd",
"currency",
"status",
"status_changed_at",
"created_at"
],
# Auto-generated display methods
display_fields=[
BadgeField(
name="internal_payment_id",
title="Payment ID",
variant="info",
icon=Icons.RECEIPT
),
UserField(
name="user",
title="User",
header=True # Show with avatar
),
CurrencyField(
name="amount_usd",
title="Amount",
currency="USD",
precision=2
),
BadgeField(
name="status",
title="Status",
label_map={
"pending": "warning",
"confirming": "info",
"confirmed": "primary",
"completed": "success",
"partially_paid": "warning",
"failed": "danger",
"cancelled": "secondary",
"expired": "danger"
}
),
DateTimeField(
name="status_changed_at",
title="Status Changed",
ordering="status_changed_at"
),
DateTimeField(
name="created_at",
title="Created",
ordering="created_at"
),
],
# Filters
list_filter=["status", "currency", "created_at"],
search_fields=["internal_payment_id", "user__username", "user__email"],
# Readonly fields (custom methods below)
readonly_fields=["payment_details_display", "qr_code_display"],
# Fieldsets
fieldsets=[
FieldsetConfig(
title="Basic Information",
fields=["id", "internal_payment_id", "user", "status"]
),
FieldsetConfig(
title="Payment Details",
fields=["amount_usd", "currency", "pay_amount", "payment_details_display"]
),
FieldsetConfig(
title="QR Code",
fields=["qr_code_display"],
collapsed=True
),
],
)
@admin.register(Payment)
class PaymentAdmin(PydanticAdmin):
"""Enhanced payment admin with custom readonly fields."""
config = payment_config
def payment_details_display(self, obj):
"""Detailed payment information using self.html utilities."""
if not obj.pk:
return "Save to see details"
details = []
# Payment age
age = timezone.now() - obj.created_at
age_text = f"{age.days} days, {age.seconds // 3600} hours"
details.append(self.html.inline([
self.html.span("Age:", "font-semibold"),
self.html.span(age_text, "")
], separator=" "))
# Transaction hash with explorer link
if obj.transaction_hash:
explorer_link = obj.get_explorer_link()
if explorer_link:
details.append(self.html.inline([
self.html.span("Transaction:", "font-semibold"),
self.html.link(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 address (monospace code)
if obj.pay_address:
details.append(self.html.inline([
self.html.span("Pay Address:", "font-semibold"),
self.html.span(f'<code>{obj.pay_address}</code>', "")
], separator=" "))
# Expiration status
if obj.expires_at:
if obj.is_expired:
details.append(self.html.badge(
f"Expired ({obj.expires_at})",
variant="danger",
icon=Icons.ERROR
))
else:
details.append(self.html.inline([
self.html.span("Expires At:", "font-semibold"),
self.html.span(str(obj.expires_at), "")
], separator=" "))
return "<br>".join(details)
payment_details_display.short_description = "Payment Details"
def qr_code_display(self, obj):
"""QR code for payment address."""
if not obj.pay_address:
return self.html.empty("No payment address")
qr_url = f"/api/payments/{obj.id}/qr/"
return f'<img src="{qr_url}" alt="QR Code" width="200" height="200">'
qr_code_display.short_description = "QR Code"User Admin
User management with computed fields, status badges, and related counts.
"""
User Admin - Complete example with computed fields
Features: @computed_field decorator, Status badges, Annotations
"""
from django.contrib import admin
from django.db.models import Count
from django_cfg.modules.django_admin import (
AdminConfig, BadgeField, DateTimeField, Icons,
UserField, computed_field, annotated_field,
)
from django_cfg.modules.django_admin.base import PydanticAdmin
# Configuration with annotations
customuser_config = AdminConfig(
model=CustomUser,
# Performance
prefetch_related=["groups", "user_permissions"],
# Annotations for counts
annotations={
'activity_count': Count('activities'),
'email_count': Count('email_logs'),
'ticket_count': Count('support_tickets'),
},
# List display
list_display=[
"avatar",
"email",
"full_name",
"status",
"activity_count_display",
"last_login",
"date_joined"
],
# Auto-generated fields
display_fields=[
UserField(name="avatar", title="Avatar", header=True),
BadgeField(name="email", title="Email", variant="info", icon=Icons.EMAIL),
DateTimeField(name="last_login", title="Last Login", ordering="last_login"),
DateTimeField(name="date_joined", title="Joined", ordering="date_joined"),
],
# Filters
list_filter=["is_staff", "is_active", "date_joined"],
search_fields=["email", "first_name", "last_name"],
)
@admin.register(CustomUser)
class UserAdmin(PydanticAdmin):
"""Enhanced user admin with computed fields."""
config = customuser_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", ordering="is_active")
def status(self, obj):
"""Enhanced status display with conditional icons and 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
)
@annotated_field("Activities", annotation_name="activity_count")
def activity_count_display(self, obj):
"""Display activity count from annotation."""
count = getattr(obj, 'activity_count', 0)
if count == 0:
return self.html.empty()
return self.html.badge(
f"{count} activit{'ies' if count != 1 else 'y'}",
variant="info",
icon=Icons.HISTORY
)E-commerce Order Admin
Order management with multiple related fields and aggregations.
"""
Order Admin - E-commerce example
Features: Multi-field display, Aggregations, Related data
"""
from django.contrib import admin
from django.db.models import Count, Sum
from django_cfg.modules.django_admin import (
AdminConfig, BadgeField, CurrencyField, DateTimeField,
FieldsetConfig, Icons, UserField, annotated_field,
)
from django_cfg.modules.django_admin.base import PydanticAdmin
order_config = AdminConfig(
model=Order,
# Performance
select_related=["user", "shipping_address"],
prefetch_related=["items"],
# Annotations
annotations={
'items_count': Count('items'),
'total_amount': Sum('items__price'),
},
# List display
list_display=[
"order_number",
"user",
"items_count_display",
"total_amount_display",
"status",
"created_at"
],
# Display fields
display_fields=[
UserField(name="user", header=True, ordering="user__username"),
BadgeField(
name="status",
label_map={
"pending": "warning",
"processing": "info",
"shipped": "primary",
"delivered": "success",
"cancelled": "danger",
},
icon=Icons.SHOPPING_CART
),
DateTimeField(name="created_at", ordering="created_at"),
],
# Fieldsets
fieldsets=[
FieldsetConfig(
title="Order Information",
fields=["order_number", "user", "status"]
),
FieldsetConfig(
title="Shipping",
fields=["shipping_address", "shipping_method", "tracking_number"]
),
FieldsetConfig(
title="Items",
fields=["order_items_display"],
collapsed=True
),
],
readonly_fields=["order_items_display"],
list_filter=["status", "created_at"],
search_fields=["order_number", "user__email"],
)
@admin.register(Order)
class OrderAdmin(PydanticAdmin):
"""E-commerce order admin."""
config = order_config
@annotated_field("Items", annotation_name="items_count")
def items_count_display(self, obj):
"""Display item count from annotation."""
count = getattr(obj, 'items_count', 0)
return self.html.badge(
f"{count} item{'s' if count != 1 else ''}",
variant="info",
icon=Icons.SHOPPING_BAG
)
@annotated_field("Total", annotation_name="total_amount", ordering="total_amount")
def total_amount_display(self, obj):
"""Display total from annotation."""
from django_cfg.modules.django_admin.utils.displays import MoneyDisplay
from django_cfg.modules.django_admin.models.display_models import MoneyDisplayConfig
total = getattr(obj, 'total_amount', 0) or 0
config = MoneyDisplayConfig(currency="USD", precision=2)
return MoneyDisplay.amount(total, config)
def order_items_display(self, obj):
"""Display order items with self.html."""
if not obj.pk:
return "Save to see items"
items = obj.items.all()
if not items:
return self.html.empty("No items")
item_lines = []
for item in items:
item_lines.append(self.html.inline([
self.html.badge(item.product.name, variant="primary"),
self.html.span(f"Ă—{item.quantity}", ""),
self.html.span(f"${item.price:.2f}", "font-mono"),
], separator=" "))
return "<br>".join(item_lines)
order_items_display.short_description = "Order Items"Newsletter Admin
Newsletter management with email tracking and status indicators.
"""
Newsletter Admin - Email campaign example
Features: Email tracking, Custom filters, Multiple computed fields
"""
from django.contrib import admin
from django_cfg.modules.django_admin import (
AdminConfig, BadgeField, DateTimeField, Icons,
UserField, computed_field,
)
from django_cfg.modules.django_admin.base import PydanticAdmin
emaillog_config = AdminConfig(
model=EmailLog,
# Performance
select_related=['user', 'newsletter'],
# List display
list_display=[
"user_display",
"recipient_display",
"subject_display",
"status_display",
"tracking_display",
"created_at"
],
# Display fields
display_fields=[
UserField(name="user", title="User"),
BadgeField(name="recipient", title="Recipient", variant="info", icon=Icons.EMAIL),
BadgeField(name="subject", title="Subject", variant="primary"),
DateTimeField(name="created_at", title="Created", ordering="created_at"),
],
# Filters
list_filter=["status", "created_at", "newsletter"],
search_fields=["recipient", "subject", "user__username"],
)
@admin.register(EmailLog)
class EmailLogAdmin(PydanticAdmin):
"""Newsletter email tracking admin."""
config = emaillog_config
@computed_field("Status", ordering="status")
def status_display(self, obj):
"""Status with icons."""
icon_map = {
'pending': Icons.SCHEDULE,
'sent': Icons.CHECK_CIRCLE,
'failed': Icons.ERROR,
}
variant_map = {
'pending': 'warning',
'sent': 'success',
'failed': 'danger',
}
return self.html.badge(
obj.get_status_display(),
variant=variant_map.get(obj.status, 'secondary'),
icon=icon_map.get(obj.status)
)
@computed_field("Tracking")
def tracking_display(self, obj):
"""Email tracking indicators."""
badges = []
if obj.is_opened:
badges.append(self.html.badge(
"Opened",
variant="success",
icon=Icons.VISIBILITY
))
if obj.is_clicked:
badges.append(self.html.badge(
"Clicked",
variant="info",
icon=Icons.MOUSE
))
if not badges:
return self.html.empty("No activity")
return self.html.inline(badges, separator=" ")Cryptocurrency Exchange Admin
Crypto exchange with smart decimal formatting and market data.
"""
Exchange Admin - Cryptocurrency example
Features: Smart decimal places, Large numbers, External links
"""
from django.contrib import admin
from django_cfg.modules.django_admin import (
AdminConfig, BadgeField, BooleanField, CurrencyField,
FieldsetConfig, Icons,
)
from django_cfg.modules.django_admin.base import PydanticAdmin
exchange_config = AdminConfig(
model=Exchange,
# List display
list_display=[
"name",
"code",
"volume_24h_usd",
"rank",
"is_active",
"is_verified"
],
# Display fields
display_fields=[
BadgeField(name="name", title="Exchange", variant="primary", icon=Icons.BUSINESS),
BadgeField(name="code", title="Code", variant="info"),
CurrencyField(
name="volume_24h_usd",
title="24h Volume",
currency="USD",
precision=0 # No decimals for large numbers
),
BooleanField(name="is_active", title="Active"),
BooleanField(name="is_verified", title="Verified"),
],
# Fieldsets
fieldsets=[
FieldsetConfig(
title="Basic Information",
fields=["name", "code", "description"]
),
FieldsetConfig(
title="Trading Data",
fields=["volume_24h_usd", "rank", "trading_pairs_count"]
),
FieldsetConfig(
title="Links",
fields=["website_link_display", "api_link_display"],
collapsed=True
),
],
readonly_fields=["website_link_display", "api_link_display"],
list_filter=["is_active", "is_verified"],
search_fields=["name", "code"],
prepopulated_fields={'slug': ('name',)},
)
@admin.register(Exchange)
class ExchangeAdmin(PydanticAdmin):
"""Cryptocurrency exchange admin."""
config = exchange_config
def website_link_display(self, obj):
"""External website link."""
if not obj.website_url:
return self.html.empty()
return self.html.link(
obj.website_url,
"Visit Website",
css_class="text-blue-600",
target="_blank"
)
website_link_display.short_description = "Website"
def api_link_display(self, obj):
"""API documentation link."""
if not obj.api_url:
return self.html.empty()
return self.html.inline([
self.html.icon(Icons.API, size="sm"),
self.html.link(obj.api_url, "API Docs", target="_blank")
])
api_link_display.short_description = "API"Wallet Balance Admin
Exchange wallet balances with UUID shortening and custom avatar displays.
"""
Wallet Balance Admin - Cryptocurrency exchange example
Features: UUID shortening, custom avatars, balance displays, SafeString HTML
"""
from decimal import Decimal
from django.contrib import admin
from django_cfg.modules.django_admin import (
AdminConfig, DateTimeField, FieldsetConfig, Icons, computed_field,
)
from django_cfg.modules.django_admin.base import PydanticAdmin
wallet_config = AdminConfig(
model=MarketAccountWallet,
# Performance optimization
select_related=[
'account', 'account__account', 'account__account__exchange',
'account__account__user', 'coin'
],
# List display with custom fields
list_display=[
'id_display', # Shortened UUID
'wallet_display', # Custom avatar + text
'coin_display', # Badge
'balance_total_display',
'balance_available_display',
'balance_locked_display',
'value_usd_display',
'last_sync_display',
],
# Filters
list_filter=[
'account__market_type',
'account__account__exchange',
'coin__is_stable',
'account__is_active',
],
# Search
search_fields=[
'id', 'coin__symbol', 'coin__label',
'account__account__name', 'account__account__user__email',
],
# Ordering by balance
ordering=['-balance_total', 'coin__symbol'],
list_per_page=50,
readonly_fields=['id', 'created_at', 'updated_at', 'last_sync_at'],
)
@admin.register(MarketAccountWallet)
class MarketAccountWalletAdmin(PydanticAdmin):
config = wallet_config
@computed_field("ID")
def id_display(self, obj):
"""Short UUID with tooltip."""
return self.html.uuid_short(obj.id, length=6)
@computed_field("Wallet")
def wallet_display(self, obj):
"""Custom avatar with account info."""
if not obj.account or not obj.account.account:
return self.html.empty("Unknown")
account = obj.account.account
market_type = obj.account.market_type
header_text = f"{account.name} ({market_type})"
subtitle_text = f"{account.exchange.name} - {account.user.email}"
initials = account.name[:2].upper()
# Use inline styles for perfect circular avatar
from django.utils.safestring import mark_safe
return mark_safe(
f'<div class="flex items-center gap-3">'
f'<div class="flex items-center justify-center rounded-full '
f'bg-primary-100 text-primary-600 font-semibold flex-shrink-0" '
f'style="width: 40px; height: 40px; min-width: 40px; min-height: 40px;">'
f'{initials}'
f'</div>'
f'<div class="flex-1 min-w-0">'
f'<div class="font-medium truncate">{header_text}</div>'
f'<div class="text-sm text-gray-500 truncate">{subtitle_text}</div>'
f'</div>'
f'</div>'
)
@computed_field("Coin")
def coin_display(self, obj):
"""Coin badge."""
if not obj.coin:
return self.html.empty("Unknown")
return self.html.badge(obj.coin.symbol, variant="info", icon=Icons.CURRENCY_BITCOIN)
@computed_field("Total")
def balance_total_display(self, obj):
"""Total balance with currency."""
if obj.balance_total <= 0:
return self.html.span("0.00", "text-gray-500")
precision = obj.coin.precision_display if obj.coin else 8
formatted = f"{obj.balance_total:.{precision}f}".rstrip('0').rstrip('.')
symbol = obj.coin.symbol if obj.coin else ''
# Use inline with empty separator for adjacent HTML
return self.html.inline([
f'<strong>{formatted}</strong>',
f'<span class="text-gray-500 ml-1">{symbol}</span>',
], separator="")
@computed_field("Available")
def balance_available_display(self, obj):
"""Available balance."""
if obj.balance_available <= 0:
return self.html.span("0.00", "text-gray-500")
precision = obj.coin.precision_display if obj.coin else 8
formatted = f"{obj.balance_available:.{precision}f}".rstrip('0').rstrip('.')
symbol = obj.coin.symbol if obj.coin else ''
return self.html.inline([
self.html.span(formatted, "text-green-600 font-semibold"),
f'<span class="text-gray-500 ml-1">{symbol}</span>',
], separator="")
@computed_field("Value USD")
def value_usd_display(self, obj):
"""USD value badge."""
if not obj.coin or not obj.coin.price_usd:
return self.html.span("—", "text-gray-500")
value_usd = obj.balance_total * obj.coin.price_usd
if value_usd < Decimal('0.01'):
return self.html.span("< $0.01", "text-gray-500")
return self.html.badge(
f"${value_usd:,.2f}",
variant="success",
icon=Icons.ATTACH_MONEY,
)Key Features:
uuid_short()for clean ID display with tooltip- Custom circular avatar with fixed proportions
inline()withseparator=""for adjacent HTML elements- Tailwind CSS classes for styling
mark_safe()for complex HTML structures- Perfect for financial/crypto applications
Common Patterns
Pattern 1: Conditional Badges
@computed_field("Priority")
def priority_display(self, obj):
"""Priority with colors based on value."""
priority_map = {
'low': ('info', Icons.ARROW_DOWNWARD),
'medium': ('warning', Icons.REMOVE),
'high': ('danger', Icons.ARROW_UPWARD),
'critical': ('danger', Icons.PRIORITY_HIGH),
}
variant, icon = priority_map.get(obj.priority, ('secondary', None))
return self.html.badge(obj.get_priority_display(), variant=variant, icon=icon)Pattern 2: Multiple Inline Badges
@computed_field("Features")
def features_display(self, obj):
"""Show multiple feature badges."""
badges = []
if obj.is_featured:
badges.append(self.html.badge("Featured", variant="success", icon=Icons.STAR))
if obj.is_new:
badges.append(self.html.badge("New", variant="info", icon=Icons.NEW_RELEASES))
if obj.on_sale:
badges.append(self.html.badge("Sale", variant="danger", icon=Icons.LOCAL_OFFER))
return self.html.inline(badges, separator=" ") if badges else self.html.empty()Pattern 3: Complex Readonly Field
def detailed_info_display(self, obj):
"""Complex info using multiple self.html methods."""
sections = []
# Header
sections.append(self.html.span("Details", "text-lg font-bold"))
# Key-value pairs
info_items = [
("ID", obj.id),
("Created", obj.created_at.strftime("%Y-%m-%d")),
("Updated", obj.updated_at.strftime("%Y-%m-%d")),
]
for label, value in info_items:
sections.append(self.html.inline([
self.html.span(f"{label}:", "font-semibold"),
self.html.span(str(value), "")
], separator=" "))
# Status badge
sections.append(self.html.badge(
obj.get_status_display(),
variant="success" if obj.is_active else "secondary"
))
return "<br>".join(sections)Time-Based Colored Text
@computed_field("Last Activity")
def last_activity_display(self, obj):
"""Show time ago with color based on freshness."""
if not obj.last_activity_at:
return self.html.empty("Never")
from django.utils import timezone
from django.utils.timesince import timesince
age_seconds = (timezone.now() - obj.last_activity_at).total_seconds()
time_ago = timesince(obj.last_activity_at, timezone.now())
# Color based on freshness
if age_seconds < 120: # < 2 minutes
color = "success"
elif age_seconds < 300: # < 5 minutes
color = "warning"
else: # > 5 minutes
color = "danger"
return self.html.colored_text(f"{time_ago} ago", color)Status with Icon and Color
@computed_field("Connection Status")
def connection_status_display(self, obj):
"""Show connection status with icon and color."""
if obj.is_connected:
return self.html.icon_text(
Icons.CHECK_CIRCLE,
"Connected",
color="success"
)
# Show disconnection time with color
if obj.last_seen_at:
from django.utils import timezone
from django.utils.timesince import timesince
age_seconds = (timezone.now() - obj.last_seen_at).total_seconds()
time_ago = timesince(obj.last_seen_at, timezone.now())
# Gradually change color as time passes
if age_seconds < 60: # < 1 minute
color = "warning"
icon = Icons.SCHEDULE
elif age_seconds < 300: # < 5 minutes
color = "warning"
icon = Icons.CANCEL
else: # > 5 minutes
color = "danger"
icon = Icons.ERROR
return self.html.icon_text(icon, f"Offline {time_ago}", color=color)
return self.html.icon_text(Icons.CANCEL, "Never connected", color="secondary")Bot/API Configuration Admin
Complete example with JSON Widget for managing bot configurations and API schemas.
"""
Bot Configuration Admin - JSON Widget example
Features: JSON editor with copy button, Read-only schemas, Multiple JSON fields
"""
from django.contrib import admin
from django_cfg.modules.django_admin import (
AdminConfig, BadgeField, BooleanField, DateTimeField,
FieldsetConfig, Icons, JSONWidgetConfig, ShortUUIDField,
)
from django_cfg.modules.django_admin.base import PydanticAdmin
# Model
class Bot(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
adapter = models.ForeignKey(Adapter, on_delete=models.CASCADE)
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
enabled = models.BooleanField(default=False)
# JSON fields
settings = models.JSONField(default=dict) # Editable configuration
config_schema = models.JSONField(null=True) # Read-only Pydantic schema
# Monitoring
last_heartbeat_at = models.DateTimeField(null=True)
is_alive = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Declarative configuration
bot_config = AdminConfig(
model=Bot,
# List display
list_display=[
"id",
"name",
"adapter_display",
"status_display",
"enabled",
"is_alive_display",
"last_heartbeat_display",
"created_at",
],
# Auto-generated display methods
display_fields=[
ShortUUIDField(
name="id",
title="ID",
length=8,
),
BadgeField(
name="status",
title="Status",
label_map={
"stopped": "secondary",
"starting": "info",
"running": "success",
"paused": "warning",
"stopping": "warning",
"error": "danger",
},
icon=Icons.PLAY_CIRCLE,
),
BooleanField(name="enabled", title="Enabled"),
BooleanField(name="is_alive", title="Alive"),
DateTimeField(name="created_at", title="Created", show_relative=True),
DateTimeField(name="last_heartbeat_at", title="Last Heartbeat", show_relative=True),
],
# JSON Widget configurations
widgets=[
# Editable settings - tree mode
JSONWidgetConfig(
field="settings",
mode="tree", # Interactive tree for editing
height="400px",
show_copy_button=True,
),
# Read-only schema - view mode
JSONWidgetConfig(
field="config_schema",
mode="view", # Read-only for Pydantic schema
height="500px", # Larger height for schema
show_copy_button=True, # Easy to copy schema
),
],
# Fieldsets
fieldsets=[
FieldsetConfig(
title="Basic Info",
fields=["id", "name", "description", "adapter"],
),
FieldsetConfig(
title="Runtime State",
fields=["status", "enabled"],
),
FieldsetConfig(
title="Monitoring",
fields=["last_heartbeat_at", "last_trade_at", "is_alive", "heartbeat_age_seconds"],
),
FieldsetConfig(
title="Settings",
fields=["settings"], # Uses tree mode from widgets
collapsed=True,
),
FieldsetConfig(
title="Config Schema",
fields=[
"config_schema", # Uses view mode from widgets
"config_schema_requested_display",
],
collapsed=True,
description="Pydantic JSON Schema from bot for dynamic UI generation",
),
FieldsetConfig(
title="Timestamps",
fields=["created_at", "updated_at"],
collapsed=True,
),
],
# Configuration
list_filter=["status", "enabled", "adapter"],
search_fields=["name", "description", "id"],
readonly_fields=[
"id", "created_at", "updated_at",
"last_heartbeat_at", "is_alive",
"config_schema", "config_schema_requested_display"
],
)
# Register admin
@admin.register(Bot)
class BotAdmin(PydanticAdmin):
"""Admin interface for bots (unified runtime state)"""
config = bot_config
# Custom computed fields
@computed_field("Adapter")
def adapter_display(self, obj):
"""Display adapter with icon."""
if obj.adapter:
return self.html.icon_text(Icons.EXTENSION, obj.adapter.name)
return self.html.empty("-")
@computed_field("Status")
def status_display(self, obj):
"""Display status with icon."""
status_icons = {
Bot.Status.STOPPED: Icons.STOP,
Bot.Status.STARTING: Icons.PLAY_ARROW,
Bot.Status.RUNNING: Icons.PLAY_CIRCLE,
Bot.Status.PAUSED: Icons.PAUSE,
Bot.Status.STOPPING: Icons.STOP_CIRCLE,
Bot.Status.ERROR: Icons.ERROR,
}
return self.html.icon_text(
status_icons.get(obj.status, Icons.HELP),
obj.get_status_display(),
)
@computed_field("Alive")
def is_alive_display(self, obj):
"""Display if bot is alive."""
if obj.is_alive:
return self.html.icon_text(Icons.CHECK_CIRCLE, "Yes", color="success")
return self.html.icon_text(Icons.CANCEL, "No", color="secondary")
@computed_field("Last Heartbeat")
def last_heartbeat_display(self, obj):
"""Display last heartbeat with relative time."""
if obj.last_heartbeat_at:
from django.utils import timezone
from django.utils.timesince import timesince
age_seconds = (timezone.now() - obj.last_heartbeat_at).total_seconds()
if age_seconds < 120: # < 2 minutes
color = "success"
elif age_seconds < 300: # < 5 minutes
color = "warning"
else:
color = "danger"
time_ago = timesince(obj.last_heartbeat_at, timezone.now())
return self.html.colored_text(f"{time_ago} ago", color)
return self.html.empty("-")
@computed_field("Schema Requested")
def config_schema_requested_display(self, obj):
"""Display when schema was requested."""
if obj.config_schema_requested_at:
from django.utils import timezone
from django.utils.timesince import timesince
time_ago = timesince(obj.config_schema_requested_at, timezone.now())
return self.html.colored_text(f"{time_ago} ago", "info")
return self.html.empty("-")Key Features
JSON Widget Configuration:
settingsfield usesmode="tree"for interactive editingconfig_schemafield usesmode="view"for read-only display- Both have
show_copy_button=Truefor easy copying - Different heights optimized for content
Centralized Widget Config:
widgets=[
JSONWidgetConfig(
field="settings",
mode="tree", # Editable
height="400px",
),
JSONWidgetConfig(
field="config_schema",
mode="view", # Read-only
height="500px",
),
]Clean Fieldsets:
# Just field names - widget config is separate!
FieldsetConfig(
title="Settings",
fields=["settings"], # Widget config applied automatically
)Next Steps
- Overview - Learn the philosophy
- Quick Start - Get started in 5 minutes
- Configuration - Complete configuration reference
- Field Types - All field types
- Filters - Complete guide to filters
Pro Tip All examples above are from real production code. Copy-paste and adapt them to your needs!