# 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( '{}', '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('{}', 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'] ```