mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:11:16 -05:00
- Created a base email template (base.html) for consistent styling across all emails. - Added moderation approval email template (moderation_approved.html) to notify users of approved submissions. - Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions. - Created password reset email template (password_reset.html) for users requesting to reset their passwords. - Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
207 lines
7.5 KiB
Python
207 lines
7.5 KiB
Python
"""
|
||
Django Admin configuration for media models.
|
||
"""
|
||
from django.contrib import admin
|
||
from django.contrib.contenttypes.admin import GenericTabularInline
|
||
from django.utils.html import format_html
|
||
from django.utils.safestring import mark_safe
|
||
from django.db.models import Count, Q
|
||
from .models import Photo
|
||
|
||
|
||
@admin.register(Photo)
|
||
class PhotoAdmin(admin.ModelAdmin):
|
||
"""Admin interface for Photo model with enhanced features."""
|
||
|
||
list_display = [
|
||
'thumbnail_preview', 'title', 'photo_type', 'moderation_status',
|
||
'entity_info', 'uploaded_by', 'dimensions', 'file_size_display', 'created'
|
||
]
|
||
list_filter = [
|
||
'moderation_status', 'is_approved', 'photo_type',
|
||
'is_featured', 'is_public', 'created'
|
||
]
|
||
search_fields = [
|
||
'title', 'description', 'cloudflare_image_id',
|
||
'uploaded_by__email', 'uploaded_by__username'
|
||
]
|
||
readonly_fields = [
|
||
'id', 'created', 'modified', 'content_type', 'object_id',
|
||
'moderated_at'
|
||
]
|
||
raw_id_fields = ['uploaded_by', 'moderated_by']
|
||
|
||
fieldsets = (
|
||
('CloudFlare Image', {
|
||
'fields': (
|
||
'cloudflare_image_id', 'cloudflare_url',
|
||
'cloudflare_thumbnail_url'
|
||
)
|
||
}),
|
||
('Metadata', {
|
||
'fields': ('title', 'description', 'credit', 'photo_type')
|
||
}),
|
||
('Associated Entity', {
|
||
'fields': ('content_type', 'object_id')
|
||
}),
|
||
('Upload Information', {
|
||
'fields': ('uploaded_by',)
|
||
}),
|
||
('Moderation', {
|
||
'fields': (
|
||
'moderation_status', 'is_approved',
|
||
'moderated_by', 'moderated_at', 'moderation_notes'
|
||
)
|
||
}),
|
||
('Image Details', {
|
||
'fields': ('width', 'height', 'file_size'),
|
||
'classes': ('collapse',)
|
||
}),
|
||
('Display Settings', {
|
||
'fields': ('display_order', 'is_featured', 'is_public')
|
||
}),
|
||
('System', {
|
||
'fields': ('id', 'created', 'modified'),
|
||
'classes': ('collapse',)
|
||
}),
|
||
)
|
||
|
||
date_hierarchy = 'created'
|
||
actions = ['approve_photos', 'reject_photos', 'flag_photos', 'make_featured', 'remove_featured']
|
||
|
||
def get_queryset(self, request):
|
||
"""Optimize queryset with select_related."""
|
||
qs = super().get_queryset(request)
|
||
return qs.select_related(
|
||
'uploaded_by', 'moderated_by', 'content_type'
|
||
)
|
||
|
||
def thumbnail_preview(self, obj):
|
||
"""Display thumbnail preview in list view."""
|
||
if obj.cloudflare_url:
|
||
# Use thumbnail variant for preview
|
||
from apps.media.services import CloudFlareService
|
||
cf = CloudFlareService()
|
||
thumbnail_url = cf.get_image_url(obj.cloudflare_image_id, 'thumbnail')
|
||
|
||
return format_html(
|
||
'<img src="{}" style="width: 60px; height: 60px; object-fit: cover; border-radius: 4px;" />',
|
||
thumbnail_url
|
||
)
|
||
return "-"
|
||
thumbnail_preview.short_description = "Preview"
|
||
|
||
def entity_info(self, obj):
|
||
"""Display entity information."""
|
||
if obj.content_type and obj.object_id:
|
||
entity = obj.content_object
|
||
if entity:
|
||
entity_type = obj.content_type.model
|
||
entity_name = getattr(entity, 'name', str(entity))
|
||
return format_html(
|
||
'<strong>{}</strong><br/><small>{}</small>',
|
||
entity_name,
|
||
entity_type.upper()
|
||
)
|
||
return format_html('<em style="color: #999;">Not attached</em>')
|
||
entity_info.short_description = "Entity"
|
||
|
||
def dimensions(self, obj):
|
||
"""Display image dimensions."""
|
||
if obj.width and obj.height:
|
||
return f"{obj.width}×{obj.height}"
|
||
return "-"
|
||
dimensions.short_description = "Size"
|
||
|
||
def file_size_display(self, obj):
|
||
"""Display file size in human-readable format."""
|
||
if obj.file_size:
|
||
size_kb = obj.file_size / 1024
|
||
if size_kb > 1024:
|
||
return f"{size_kb / 1024:.1f} MB"
|
||
return f"{size_kb:.1f} KB"
|
||
return "-"
|
||
file_size_display.short_description = "File Size"
|
||
|
||
def changelist_view(self, request, extra_context=None):
|
||
"""Add statistics to changelist."""
|
||
extra_context = extra_context or {}
|
||
|
||
# Get photo statistics
|
||
stats = Photo.objects.aggregate(
|
||
total=Count('id'),
|
||
pending=Count('id', filter=Q(moderation_status='pending')),
|
||
approved=Count('id', filter=Q(moderation_status='approved')),
|
||
rejected=Count('id', filter=Q(moderation_status='rejected')),
|
||
flagged=Count('id', filter=Q(moderation_status='flagged')),
|
||
)
|
||
|
||
extra_context['photo_stats'] = stats
|
||
|
||
return super().changelist_view(request, extra_context)
|
||
|
||
def approve_photos(self, request, queryset):
|
||
"""Bulk approve selected photos."""
|
||
count = 0
|
||
for photo in queryset:
|
||
photo.approve(moderator=request.user, notes='Bulk approved')
|
||
count += 1
|
||
self.message_user(request, f"{count} photo(s) approved successfully.")
|
||
approve_photos.short_description = "Approve selected photos"
|
||
|
||
def reject_photos(self, request, queryset):
|
||
"""Bulk reject selected photos."""
|
||
count = 0
|
||
for photo in queryset:
|
||
photo.reject(moderator=request.user, notes='Bulk rejected')
|
||
count += 1
|
||
self.message_user(request, f"{count} photo(s) rejected.")
|
||
reject_photos.short_description = "Reject selected photos"
|
||
|
||
def flag_photos(self, request, queryset):
|
||
"""Bulk flag selected photos for review."""
|
||
count = 0
|
||
for photo in queryset:
|
||
photo.flag(moderator=request.user, notes='Flagged for review')
|
||
count += 1
|
||
self.message_user(request, f"{count} photo(s) flagged for review.")
|
||
flag_photos.short_description = "Flag selected photos"
|
||
|
||
def make_featured(self, request, queryset):
|
||
"""Mark selected photos as featured."""
|
||
count = queryset.update(is_featured=True)
|
||
self.message_user(request, f"{count} photo(s) marked as featured.")
|
||
make_featured.short_description = "Mark as featured"
|
||
|
||
def remove_featured(self, request, queryset):
|
||
"""Remove featured status from selected photos."""
|
||
count = queryset.update(is_featured=False)
|
||
self.message_user(request, f"{count} photo(s) removed from featured.")
|
||
remove_featured.short_description = "Remove featured status"
|
||
|
||
|
||
# Inline admin for use in entity admin pages
|
||
class PhotoInline(GenericTabularInline):
|
||
"""Inline admin for photos in entity pages."""
|
||
model = Photo
|
||
ct_field = 'content_type'
|
||
ct_fk_field = 'object_id'
|
||
extra = 0
|
||
fields = ['thumbnail_preview', 'title', 'photo_type', 'moderation_status', 'display_order']
|
||
readonly_fields = ['thumbnail_preview']
|
||
can_delete = True
|
||
|
||
def thumbnail_preview(self, obj):
|
||
"""Display thumbnail preview in inline."""
|
||
if obj.cloudflare_url:
|
||
from apps.media.services import CloudFlareService
|
||
cf = CloudFlareService()
|
||
thumbnail_url = cf.get_image_url(obj.cloudflare_image_id, 'thumbnail')
|
||
|
||
return format_html(
|
||
'<img src="{}" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" />',
|
||
thumbnail_url
|
||
)
|
||
return "-"
|
||
thumbnail_preview.short_description = "Preview"
|