mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 13:11: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.
203 lines
6.8 KiB
Python
203 lines
6.8 KiB
Python
"""
|
|
Django admin configuration for the Media (shared) application.
|
|
|
|
This module provides admin interfaces for photo management with
|
|
bulk operations and content type linking.
|
|
|
|
Performance targets:
|
|
- List views: < 10 queries
|
|
- Page load time: < 500ms for 100 records
|
|
"""
|
|
|
|
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
|
|
from .models import Photo
|
|
|
|
|
|
@admin.register(Photo)
|
|
class PhotoAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for Photo management.
|
|
|
|
Provides photo administration with:
|
|
- Thumbnail previews in list view
|
|
- Content type linking
|
|
- Bulk primary photo management
|
|
- Alt text validation warnings
|
|
|
|
Query optimizations:
|
|
- select_related: content_type
|
|
"""
|
|
|
|
list_display = (
|
|
"thumbnail_preview",
|
|
"content_type_display",
|
|
"content_object_link",
|
|
"caption_preview",
|
|
"is_primary_badge",
|
|
"has_alt_text",
|
|
"created_at",
|
|
)
|
|
list_filter = ("content_type", "is_primary", "created_at")
|
|
list_select_related = ["content_type"]
|
|
search_fields = ("caption", "alt_text", "object_id")
|
|
readonly_fields = ("thumbnail_preview", "created_at", "updated_at")
|
|
list_per_page = 50
|
|
show_full_result_count = False
|
|
ordering = ("-created_at",)
|
|
|
|
fieldsets = (
|
|
(
|
|
"Image",
|
|
{
|
|
"fields": ("image", "thumbnail_preview"),
|
|
"description": "Upload and preview the photo.",
|
|
},
|
|
),
|
|
(
|
|
"Related Object",
|
|
{
|
|
"fields": ("content_type", "object_id"),
|
|
"description": "The object this photo belongs to (park, ride, etc.).",
|
|
},
|
|
),
|
|
(
|
|
"Photo Details",
|
|
{
|
|
"fields": ("caption", "alt_text", "is_primary"),
|
|
"description": "Caption and accessibility information.",
|
|
},
|
|
),
|
|
(
|
|
"Metadata",
|
|
{
|
|
"fields": ("created_at", "updated_at"),
|
|
"classes": ("collapse",),
|
|
},
|
|
),
|
|
)
|
|
|
|
@admin.display(description="Preview")
|
|
def thumbnail_preview(self, obj):
|
|
"""Display thumbnail preview of the photo."""
|
|
if obj.image:
|
|
return format_html(
|
|
'<img src="{}" style="max-height: 60px; max-width: 100px; '
|
|
'border-radius: 4px; object-fit: cover;" loading="lazy" />',
|
|
obj.image.url,
|
|
)
|
|
return format_html('<span style="color: gray;">No image</span>')
|
|
|
|
@admin.display(description="Type")
|
|
def content_type_display(self, obj):
|
|
"""Display content type in a readable format."""
|
|
if obj.content_type:
|
|
return f"{obj.content_type.app_label}.{obj.content_type.model}"
|
|
return "-"
|
|
|
|
@admin.display(description="Object")
|
|
def content_object_link(self, obj):
|
|
"""Create a link to the related object's admin page."""
|
|
try:
|
|
content_obj = obj.content_object
|
|
if content_obj:
|
|
from django.urls import reverse
|
|
|
|
app_label = obj.content_type.app_label
|
|
model_name = obj.content_type.model
|
|
try:
|
|
url = reverse(
|
|
f"admin:{app_label}_{model_name}_change",
|
|
args=[content_obj.pk],
|
|
)
|
|
return format_html(
|
|
'<a href="{}">{}</a>',
|
|
url,
|
|
str(content_obj)[:30],
|
|
)
|
|
except Exception:
|
|
return str(content_obj)[:30]
|
|
return "-"
|
|
except Exception:
|
|
return format_html('<span style="color: red;">Not found</span>')
|
|
|
|
@admin.display(description="Caption")
|
|
def caption_preview(self, obj):
|
|
"""Display truncated caption."""
|
|
if obj.caption:
|
|
return obj.caption[:40] + "..." if len(obj.caption) > 40 else obj.caption
|
|
return "-"
|
|
|
|
@admin.display(description="Primary")
|
|
def is_primary_badge(self, obj):
|
|
"""Display primary status with badge."""
|
|
if obj.is_primary:
|
|
return format_html(
|
|
'<span style="background-color: green; color: white; padding: 2px 8px; '
|
|
'border-radius: 4px; font-size: 11px;">Primary</span>'
|
|
)
|
|
return format_html(
|
|
'<span style="background-color: gray; color: white; padding: 2px 8px; '
|
|
'border-radius: 4px; font-size: 11px;">-</span>'
|
|
)
|
|
|
|
@admin.display(description="Alt", boolean=True)
|
|
def has_alt_text(self, obj):
|
|
"""Indicate if photo has alt text for accessibility."""
|
|
return bool(obj.alt_text)
|
|
|
|
@admin.action(description="Set as primary photo")
|
|
def set_primary(self, request, queryset):
|
|
"""Set selected photos as primary for their objects."""
|
|
for photo in queryset:
|
|
# Unset other primary photos for the same object
|
|
Photo.objects.filter(
|
|
content_type=photo.content_type,
|
|
object_id=photo.object_id,
|
|
is_primary=True,
|
|
).exclude(pk=photo.pk).update(is_primary=False)
|
|
photo.is_primary = True
|
|
photo.save(update_fields=["is_primary"])
|
|
self.message_user(request, f"Set {queryset.count()} photos as primary.")
|
|
|
|
@admin.action(description="Remove primary status")
|
|
def remove_primary(self, request, queryset):
|
|
"""Remove primary status from selected photos."""
|
|
updated = queryset.update(is_primary=False)
|
|
self.message_user(request, f"Removed primary status from {updated} photos.")
|
|
|
|
@admin.action(description="Flag missing alt text")
|
|
def flag_missing_alt(self, request, queryset):
|
|
"""List photos missing alt text for accessibility."""
|
|
missing = queryset.filter(alt_text__isnull=True) | queryset.filter(alt_text="")
|
|
count = missing.count()
|
|
if count:
|
|
self.message_user(
|
|
request,
|
|
f"Found {count} photos missing alt text. Please add alt text for accessibility.",
|
|
level="WARNING",
|
|
)
|
|
else:
|
|
self.message_user(request, "All selected photos have alt text.")
|
|
|
|
def get_actions(self, request):
|
|
"""Add custom actions to the admin."""
|
|
actions = super().get_actions(request)
|
|
actions["set_primary"] = (
|
|
self.set_primary,
|
|
"set_primary",
|
|
"Set as primary photo",
|
|
)
|
|
actions["remove_primary"] = (
|
|
self.remove_primary,
|
|
"remove_primary",
|
|
"Remove primary status",
|
|
)
|
|
actions["flag_missing_alt"] = (
|
|
self.flag_missing_alt,
|
|
"flag_missing_alt",
|
|
"Flag missing alt text",
|
|
)
|
|
return actions
|