# 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']
```
