Files
thrillwiki_django_no_react/docs/admin/best_practices.md
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- 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.
2025-12-23 16:41:42 -05:00

268 lines
6.2 KiB
Markdown

# 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
```python
class RideAdmin(BaseModelAdmin):
list_display = ['name', 'park', 'manufacturer']
list_select_related = ['park', 'manufacturer'] # Prevents N+1 queries
```
### Use prefetch_related for Reverse Relations
```python
class ParkAdmin(BaseModelAdmin):
list_display = ['name', 'ride_count']
list_prefetch_related = ['rides'] # For efficient count queries
```
### Use Annotations for Calculated Fields
```python
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
```python
@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
```python
@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
1. Basic Information - Name, slug, description
2. Relationships - ForeignKeys, ManyToMany
3. Status & Dates - Status fields, timestamps
4. Specifications - Technical details (collapsed)
5. Media - Images, photos (collapsed)
6. Metadata - created_at, updated_at (collapsed)
### Include Descriptions
```python
fieldsets = (
(
"Basic Information",
{
"fields": ("name", "slug", "description"),
"description": "Core identification for this record.",
},
),
)
```
## Custom Actions
### Use Proper Action Decorator
```python
@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
```python
@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
```python
@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
```python
class RankingAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
"""Rankings are calculated automatically - no manual editing."""
pass
```
### Field-Level Permissions
```python
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
```python
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
```python
class MyAdmin(BaseModelAdmin):
show_full_result_count = False # Faster for large datasets
```
### Use Pagination
```python
class MyAdmin(BaseModelAdmin):
list_per_page = 50 # Reasonable page size
```
### Limit Inline Items
```python
class ItemInline(admin.TabularInline):
model = Item
extra = 1 # Only show 1 empty form
max_num = 20 # Limit total items
```
## Testing
### Test Query Counts
```python
def test_list_view_query_count(self):
with self.assertNumQueries(10): # Set target
response = self.client.get('/admin/app/model/')
```
### Test Permissions
```python
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
```python
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
```python
# 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
```python
# 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
```python
# 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']
```