mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 16:51:09 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
6.2 KiB
6.2 KiB
Admin Best Practices
This guide outlines best practices for developing and maintaining Django admin interfaces in ThrillWiki.
Query Optimization
Always Use select_related for ForeignKeys
class RideAdmin(BaseModelAdmin):
list_display = ['name', 'park', 'manufacturer']
list_select_related = ['park', 'manufacturer'] # Prevents N+1 queries
Use prefetch_related for Reverse Relations
class ParkAdmin(BaseModelAdmin):
list_display = ['name', 'ride_count']
list_prefetch_related = ['rides'] # For efficient count queries
Use Annotations for Calculated Fields
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(
_ride_count=Count('rides', distinct=True),
_avg_rating=Avg('reviews__rating'),
)
Display Methods
Always Use format_html for Safety
@admin.display(description="Status")
def status_badge(self, obj):
return format_html(
'<span style="color: {};">{}</span>',
'green' if obj.is_active else 'red',
obj.get_status_display(),
)
Handle Missing Data Gracefully
@admin.display(description="Park")
def park_link(self, obj):
if obj.park:
url = reverse("admin:parks_park_change", args=[obj.park.pk])
return format_html('<a href="{}">{}</a>', url, obj.park.name)
return "-" # Or format_html for styled "N/A"
Fieldsets Organization
Standard Fieldset Structure
- Basic Information - Name, slug, description
- Relationships - ForeignKeys, ManyToMany
- Status & Dates - Status fields, timestamps
- Specifications - Technical details (collapsed)
- Media - Images, photos (collapsed)
- Metadata - created_at, updated_at (collapsed)
Include Descriptions
fieldsets = (
(
"Basic Information",
{
"fields": ("name", "slug", "description"),
"description": "Core identification for this record.",
},
),
)
Custom Actions
Use Proper Action Decorator
@admin.action(description="Approve selected items")
def bulk_approve(self, request, queryset):
count = queryset.update(status='approved')
self.message_user(request, f"Approved {count} items.")
Handle Errors Gracefully
@admin.action(description="Process selected items")
def process_items(self, request, queryset):
processed = 0
errors = 0
for item in queryset:
try:
item.process()
processed += 1
except Exception as e:
errors += 1
self.message_user(
request,
f"Error processing {item}: {e}",
level=messages.ERROR,
)
if processed:
self.message_user(request, f"Processed {processed} items.")
Protect Against Dangerous Operations
@admin.action(description="Ban selected users")
def ban_users(self, request, queryset):
# Prevent banning self
queryset = queryset.exclude(pk=request.user.pk)
# Prevent banning superusers
queryset = queryset.exclude(is_superuser=True)
updated = queryset.update(is_banned=True)
self.message_user(request, f"Banned {updated} users.")
Permissions
Read-Only for Auto-Generated Data
class RankingAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
"""Rankings are calculated automatically - no manual editing."""
pass
Field-Level Permissions
def get_readonly_fields(self, request, obj=None):
readonly = list(super().get_readonly_fields(request, obj))
if not request.user.is_superuser:
readonly.extend(['sensitive_field', 'admin_notes'])
return readonly
Object-Level Permissions
def has_change_permission(self, request, obj=None):
if obj is None:
return super().has_change_permission(request)
# Only allow editing own content or moderator access
if obj.user == request.user:
return True
return request.user.has_perm('app.moderate_content')
Performance Tips
Disable Full Result Count
class MyAdmin(BaseModelAdmin):
show_full_result_count = False # Faster for large datasets
Use Pagination
class MyAdmin(BaseModelAdmin):
list_per_page = 50 # Reasonable page size
Limit Inline Items
class ItemInline(admin.TabularInline):
model = Item
extra = 1 # Only show 1 empty form
max_num = 20 # Limit total items
Testing
Test Query Counts
def test_list_view_query_count(self):
with self.assertNumQueries(10): # Set target
response = self.client.get('/admin/app/model/')
Test Permissions
def test_readonly_permissions(self):
request = self.factory.get('/admin/')
request.user = User(is_superuser=False)
assert self.admin.has_add_permission(request) is False
assert self.admin.has_change_permission(request) is False
Test Custom Actions
def test_bulk_approve_action(self):
items = [create_item(status='pending') for _ in range(5)]
queryset = Item.objects.filter(pk__in=[i.pk for i in items])
self.admin.bulk_approve(self.request, queryset)
for item in items:
item.refresh_from_db()
assert item.status == 'approved'
Common Pitfalls
Avoid N+1 Queries
# BAD: Creates N+1 queries
@admin.display()
def related_count(self, obj):
return obj.related_set.count()
# GOOD: Use annotation
def get_queryset(self, request):
return super().get_queryset(request).annotate(
_related_count=Count('related_set')
)
@admin.display()
def related_count(self, obj):
return obj._related_count
Don't Forget Error Handling
# BAD: May raise AttributeError
@admin.display()
def user_name(self, obj):
return obj.user.username
# GOOD: Handle missing relations
@admin.display()
def user_name(self, obj):
return obj.user.username if obj.user else "-"
Use Autocomplete for Large Relations
# BAD: Loads all options
class MyAdmin(BaseModelAdmin):
raw_id_fields = ['park'] # Works but poor UX
# GOOD: Search with autocomplete
class MyAdmin(BaseModelAdmin):
autocomplete_fields = ['park']