Files
thrillwiki_django_no_react/backend/apps/moderation/admin.py
pacnpal 7ba0004c93 chore: fix pghistory migration deps and improve htmx utilities
- Update pghistory dependency from 0007 to 0006 in account migrations
- Add docstrings and remove unused imports in htmx_forms.py
- Add DJANGO_SETTINGS_MODULE bash commands to Claude settings
- Add state transition definitions for ride statuses
2025-12-21 17:33:24 -05:00

236 lines
7.1 KiB
Python

from django.contrib import admin
from django.contrib.admin import AdminSite
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from django_fsm_log.models import StateLog
from .models import EditSubmission, PhotoSubmission
class ModerationAdminSite(AdminSite):
site_header = "ThrillWiki Moderation"
site_title = "ThrillWiki Moderation"
index_title = "Moderation Dashboard"
def has_permission(self, request):
"""Only allow moderators and above to access this admin site"""
return request.user.is_authenticated and request.user.role in [
"MODERATOR",
"ADMIN",
"SUPERUSER",
]
moderation_site = ModerationAdminSite(name="moderation")
class EditSubmissionAdmin(admin.ModelAdmin):
list_display = [
"id",
"user_link",
"content_type",
"content_link",
"status",
"created_at",
"handled_by",
]
list_filter = ["status", "content_type", "created_at"]
search_fields = ["user__username", "reason", "source", "notes"]
readonly_fields = [
"user",
"content_type",
"object_id",
"changes",
"created_at",
]
def user_link(self, obj):
url = reverse("admin:accounts_user_change", args=[obj.user.id])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
user_link.short_description = "User"
def content_link(self, obj):
if hasattr(obj.content_object, "get_absolute_url"):
url = obj.content_object.get_absolute_url()
return format_html('<a href="{}">{}</a>', url, str(obj.content_object))
return str(obj.content_object)
content_link.short_description = "Content"
def save_model(self, request, obj, form, change):
if "status" in form.changed_data:
if obj.status == "APPROVED":
obj.approve(request.user)
elif obj.status == "REJECTED":
obj.reject(request.user)
elif obj.status == "ESCALATED":
obj.escalate(request.user)
super().save_model(request, obj, form, change)
class PhotoSubmissionAdmin(admin.ModelAdmin):
list_display = [
"id",
"user_link",
"content_type",
"content_link",
"photo_preview",
"status",
"created_at",
"handled_by",
]
list_filter = ["status", "content_type", "created_at"]
search_fields = ["user__username", "caption", "notes"]
readonly_fields = [
"user",
"content_type",
"object_id",
"photo_preview",
"created_at",
]
def user_link(self, obj):
url = reverse("admin:accounts_user_change", args=[obj.user.id])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
user_link.short_description = "User"
def content_link(self, obj):
if hasattr(obj.content_object, "get_absolute_url"):
url = obj.content_object.get_absolute_url()
return format_html('<a href="{}">{}</a>', url, str(obj.content_object))
return str(obj.content_object)
content_link.short_description = "Content"
def photo_preview(self, obj):
if obj.photo:
return format_html(
'<img src="{}" style="max-height: 100px; max-width: 200px;" />',
obj.photo.url,
)
return ""
photo_preview.short_description = "Photo Preview"
def save_model(self, request, obj, form, change):
if "status" in form.changed_data:
if obj.status == "APPROVED":
obj.approve(request.user, obj.notes)
elif obj.status == "REJECTED":
obj.reject(request.user, obj.notes)
super().save_model(request, obj, form, change)
class HistoryEventAdmin(admin.ModelAdmin):
"""Admin interface for viewing model history events"""
list_display = [
"pgh_label",
"pgh_created_at",
"get_object_link",
"get_context",
]
list_filter = ["pgh_label", "pgh_created_at"]
readonly_fields = [
"pgh_label",
"pgh_obj_id",
"pgh_data",
"pgh_context",
"pgh_created_at",
]
date_hierarchy = "pgh_created_at"
def get_object_link(self, obj):
"""Display a link to the related object if possible"""
if obj.pgh_obj and hasattr(obj.pgh_obj, "get_absolute_url"):
url = obj.pgh_obj.get_absolute_url()
return format_html('<a href="{}">{}</a>', url, str(obj.pgh_obj))
return str(obj.pgh_obj or "")
get_object_link.short_description = "Object"
def get_context(self, obj):
"""Format the context data nicely"""
if not obj.pgh_context:
return "-"
html = ["<table>"]
for key, value in obj.pgh_context.items():
html.append(f"<tr><th>{key}</th><td>{value}</td></tr>")
html.append("</table>")
return mark_safe("".join(html))
get_context.short_description = "Context"
class StateLogAdmin(admin.ModelAdmin):
"""Admin interface for FSM transition logs."""
list_display = [
'id',
'timestamp',
'get_model_name',
'get_object_link',
'state',
'transition',
'get_user_link',
]
list_filter = [
'content_type',
'state',
'transition',
'timestamp',
]
search_fields = [
'state',
'transition',
'description',
'by__username',
]
readonly_fields = [
'timestamp',
'content_type',
'object_id',
'state',
'transition',
'by',
'description',
]
date_hierarchy = 'timestamp'
ordering = ['-timestamp']
def get_model_name(self, obj):
"""Get the model name from content type."""
return obj.content_type.model
get_model_name.short_description = 'Model'
def get_object_link(self, obj):
"""Create link to the actual object."""
if obj.content_object:
# Try to get absolute URL if available
if hasattr(obj.content_object, 'get_absolute_url'):
url = obj.content_object.get_absolute_url()
else:
url = '#'
return format_html('<a href="{}">{}</a>', url, str(obj.content_object))
return f"ID: {obj.object_id}"
get_object_link.short_description = 'Object'
def get_user_link(self, obj):
"""Create link to the user who performed the transition."""
if obj.by:
url = reverse('admin:accounts_user_change', args=[obj.by.id])
return format_html('<a href="{}">{}</a>', url, obj.by.username)
return '-'
get_user_link.short_description = 'User'
# Register with moderation site only
moderation_site.register(EditSubmission, EditSubmissionAdmin)
moderation_site.register(PhotoSubmission, PhotoSubmissionAdmin)
moderation_site.register(StateLog, StateLogAdmin)
# We will register concrete event models as they are created during migrations
# Example: moderation_site.register(DesignerEvent, HistoryEventAdmin)