mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-25 07:11:09 -05:00
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.
This commit is contained in:
267
docs/admin/best_practices.md
Normal file
267
docs/admin/best_practices.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# 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']
|
||||
```
|
||||
Reference in New Issue
Block a user