mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:51:13 -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.
425 lines
11 KiB
Python
425 lines
11 KiB
Python
"""
|
||
Django admin for moderation models.
|
||
"""
|
||
from django.contrib import admin
|
||
from django.utils.html import format_html
|
||
from django.urls import reverse
|
||
from django.utils import timezone
|
||
from unfold.admin import ModelAdmin
|
||
from unfold.decorators import display
|
||
|
||
from apps.moderation.models import ContentSubmission, SubmissionItem, ModerationLock
|
||
|
||
|
||
@admin.register(ContentSubmission)
|
||
class ContentSubmissionAdmin(ModelAdmin):
|
||
"""Admin for ContentSubmission model."""
|
||
|
||
list_display = [
|
||
'title_with_icon',
|
||
'status_badge',
|
||
'entity_info',
|
||
'user',
|
||
'items_summary',
|
||
'locked_info',
|
||
'created',
|
||
]
|
||
|
||
list_filter = [
|
||
'status',
|
||
'submission_type',
|
||
'entity_type',
|
||
'created',
|
||
]
|
||
|
||
search_fields = [
|
||
'title',
|
||
'description',
|
||
'user__email',
|
||
'user__username',
|
||
]
|
||
|
||
readonly_fields = [
|
||
'id',
|
||
'status',
|
||
'entity_type',
|
||
'entity_id',
|
||
'locked_by',
|
||
'locked_at',
|
||
'reviewed_by',
|
||
'reviewed_at',
|
||
'created',
|
||
'modified',
|
||
]
|
||
|
||
fieldsets = (
|
||
('Submission Info', {
|
||
'fields': (
|
||
'id',
|
||
'title',
|
||
'description',
|
||
'submission_type',
|
||
'status',
|
||
)
|
||
}),
|
||
('Entity', {
|
||
'fields': (
|
||
'entity_type',
|
||
'entity_id',
|
||
)
|
||
}),
|
||
('User Info', {
|
||
'fields': (
|
||
'user',
|
||
'source',
|
||
'ip_address',
|
||
'user_agent',
|
||
)
|
||
}),
|
||
('Review Info', {
|
||
'fields': (
|
||
'locked_by',
|
||
'locked_at',
|
||
'reviewed_by',
|
||
'reviewed_at',
|
||
'rejection_reason',
|
||
)
|
||
}),
|
||
('Metadata', {
|
||
'fields': (
|
||
'metadata',
|
||
'created',
|
||
'modified',
|
||
),
|
||
'classes': ('collapse',)
|
||
}),
|
||
)
|
||
|
||
@display(description='Title', ordering='title')
|
||
def title_with_icon(self, obj):
|
||
"""Display title with submission type icon."""
|
||
icons = {
|
||
'create': '➕',
|
||
'update': '✏️',
|
||
'delete': '🗑️',
|
||
}
|
||
icon = icons.get(obj.submission_type, '📝')
|
||
return f"{icon} {obj.title}"
|
||
|
||
@display(description='Status', ordering='status')
|
||
def status_badge(self, obj):
|
||
"""Display colored status badge."""
|
||
colors = {
|
||
'draft': 'gray',
|
||
'pending': 'blue',
|
||
'reviewing': 'orange',
|
||
'approved': 'green',
|
||
'rejected': 'red',
|
||
}
|
||
color = colors.get(obj.status, 'gray')
|
||
return format_html(
|
||
'<span style="background-color: {}; color: white; padding: 3px 8px; '
|
||
'border-radius: 3px; font-size: 11px; font-weight: bold;">{}</span>',
|
||
color,
|
||
obj.get_status_display()
|
||
)
|
||
|
||
@display(description='Entity')
|
||
def entity_info(self, obj):
|
||
"""Display entity type and ID."""
|
||
return f"{obj.entity_type.model} #{str(obj.entity_id)[:8]}"
|
||
|
||
@display(description='Items')
|
||
def items_summary(self, obj):
|
||
"""Display item counts."""
|
||
total = obj.get_items_count()
|
||
approved = obj.get_approved_items_count()
|
||
rejected = obj.get_rejected_items_count()
|
||
pending = total - approved - rejected
|
||
|
||
return format_html(
|
||
'<span title="Pending">{}</span> / '
|
||
'<span style="color: green;" title="Approved">{}</span> / '
|
||
'<span style="color: red;" title="Rejected">{}</span>',
|
||
pending, approved, rejected
|
||
)
|
||
|
||
@display(description='Lock Status')
|
||
def locked_info(self, obj):
|
||
"""Display lock information."""
|
||
if obj.locked_by:
|
||
is_expired = not obj.is_locked()
|
||
status = '🔓 Expired' if is_expired else '🔒 Locked'
|
||
return f"{status} by {obj.locked_by.email}"
|
||
return '✅ Unlocked'
|
||
|
||
def get_queryset(self, request):
|
||
"""Optimize queryset with select_related."""
|
||
qs = super().get_queryset(request)
|
||
return qs.select_related(
|
||
'user',
|
||
'entity_type',
|
||
'locked_by',
|
||
'reviewed_by'
|
||
).prefetch_related('items')
|
||
|
||
|
||
class SubmissionItemInline(admin.TabularInline):
|
||
"""Inline admin for submission items."""
|
||
model = SubmissionItem
|
||
extra = 0
|
||
fields = [
|
||
'field_label',
|
||
'old_value_display',
|
||
'new_value_display',
|
||
'change_type',
|
||
'status',
|
||
'reviewed_by',
|
||
]
|
||
readonly_fields = [
|
||
'field_label',
|
||
'old_value_display',
|
||
'new_value_display',
|
||
'change_type',
|
||
'status',
|
||
'reviewed_by',
|
||
]
|
||
can_delete = False
|
||
|
||
def has_add_permission(self, request, obj=None):
|
||
return False
|
||
|
||
|
||
@admin.register(SubmissionItem)
|
||
class SubmissionItemAdmin(ModelAdmin):
|
||
"""Admin for SubmissionItem model."""
|
||
|
||
list_display = [
|
||
'field_label',
|
||
'submission_title',
|
||
'change_type_badge',
|
||
'status_badge',
|
||
'old_value_display',
|
||
'new_value_display',
|
||
'reviewed_by',
|
||
]
|
||
|
||
list_filter = [
|
||
'status',
|
||
'change_type',
|
||
'is_required',
|
||
'created',
|
||
]
|
||
|
||
search_fields = [
|
||
'field_name',
|
||
'field_label',
|
||
'submission__title',
|
||
]
|
||
|
||
readonly_fields = [
|
||
'id',
|
||
'submission',
|
||
'field_name',
|
||
'field_label',
|
||
'old_value',
|
||
'new_value',
|
||
'old_value_display',
|
||
'new_value_display',
|
||
'status',
|
||
'reviewed_by',
|
||
'reviewed_at',
|
||
'created',
|
||
'modified',
|
||
]
|
||
|
||
fieldsets = (
|
||
('Item Info', {
|
||
'fields': (
|
||
'id',
|
||
'submission',
|
||
'field_name',
|
||
'field_label',
|
||
'change_type',
|
||
'is_required',
|
||
'order',
|
||
)
|
||
}),
|
||
('Values', {
|
||
'fields': (
|
||
'old_value',
|
||
'new_value',
|
||
'old_value_display',
|
||
'new_value_display',
|
||
)
|
||
}),
|
||
('Review Info', {
|
||
'fields': (
|
||
'status',
|
||
'reviewed_by',
|
||
'reviewed_at',
|
||
'rejection_reason',
|
||
)
|
||
}),
|
||
('Timestamps', {
|
||
'fields': (
|
||
'created',
|
||
'modified',
|
||
)
|
||
}),
|
||
)
|
||
|
||
@display(description='Submission')
|
||
def submission_title(self, obj):
|
||
"""Display submission title with link."""
|
||
url = reverse('admin:moderation_contentsubmission_change', args=[obj.submission.id])
|
||
return format_html('<a href="{}">{}</a>', url, obj.submission.title)
|
||
|
||
@display(description='Type', ordering='change_type')
|
||
def change_type_badge(self, obj):
|
||
"""Display colored change type badge."""
|
||
colors = {
|
||
'add': 'green',
|
||
'modify': 'blue',
|
||
'remove': 'red',
|
||
}
|
||
color = colors.get(obj.change_type, 'gray')
|
||
return format_html(
|
||
'<span style="background-color: {}; color: white; padding: 2px 6px; '
|
||
'border-radius: 3px; font-size: 10px;">{}</span>',
|
||
color,
|
||
obj.get_change_type_display()
|
||
)
|
||
|
||
@display(description='Status', ordering='status')
|
||
def status_badge(self, obj):
|
||
"""Display colored status badge."""
|
||
colors = {
|
||
'pending': 'orange',
|
||
'approved': 'green',
|
||
'rejected': 'red',
|
||
}
|
||
color = colors.get(obj.status, 'gray')
|
||
return format_html(
|
||
'<span style="background-color: {}; color: white; padding: 2px 6px; '
|
||
'border-radius: 3px; font-size: 10px;">{}</span>',
|
||
color,
|
||
obj.get_status_display()
|
||
)
|
||
|
||
def get_queryset(self, request):
|
||
"""Optimize queryset with select_related."""
|
||
qs = super().get_queryset(request)
|
||
return qs.select_related('submission', 'reviewed_by')
|
||
|
||
|
||
@admin.register(ModerationLock)
|
||
class ModerationLockAdmin(ModelAdmin):
|
||
"""Admin for ModerationLock model."""
|
||
|
||
list_display = [
|
||
'submission_title',
|
||
'locked_by',
|
||
'locked_at',
|
||
'expires_at',
|
||
'status_indicator',
|
||
'lock_duration',
|
||
]
|
||
|
||
list_filter = [
|
||
'is_active',
|
||
'locked_at',
|
||
'expires_at',
|
||
]
|
||
|
||
search_fields = [
|
||
'submission__title',
|
||
'locked_by__email',
|
||
'locked_by__username',
|
||
]
|
||
|
||
readonly_fields = [
|
||
'id',
|
||
'submission',
|
||
'locked_by',
|
||
'locked_at',
|
||
'expires_at',
|
||
'is_active',
|
||
'released_at',
|
||
'lock_duration',
|
||
'is_expired_display',
|
||
'created',
|
||
'modified',
|
||
]
|
||
|
||
fieldsets = (
|
||
('Lock Info', {
|
||
'fields': (
|
||
'id',
|
||
'submission',
|
||
'locked_by',
|
||
'is_active',
|
||
)
|
||
}),
|
||
('Timing', {
|
||
'fields': (
|
||
'locked_at',
|
||
'expires_at',
|
||
'released_at',
|
||
'lock_duration',
|
||
'is_expired_display',
|
||
)
|
||
}),
|
||
('Timestamps', {
|
||
'fields': (
|
||
'created',
|
||
'modified',
|
||
)
|
||
}),
|
||
)
|
||
|
||
@display(description='Submission')
|
||
def submission_title(self, obj):
|
||
"""Display submission title with link."""
|
||
url = reverse('admin:moderation_contentsubmission_change', args=[obj.submission.id])
|
||
return format_html('<a href="{}">{}</a>', url, obj.submission.title)
|
||
|
||
@display(description='Status')
|
||
def status_indicator(self, obj):
|
||
"""Display lock status."""
|
||
if not obj.is_active:
|
||
return format_html(
|
||
'<span style="color: gray;">🔓 Released</span>'
|
||
)
|
||
elif obj.is_expired():
|
||
return format_html(
|
||
'<span style="color: orange;">⏰ Expired</span>'
|
||
)
|
||
else:
|
||
return format_html(
|
||
'<span style="color: green;">🔒 Active</span>'
|
||
)
|
||
|
||
@display(description='Duration')
|
||
def lock_duration(self, obj):
|
||
"""Display lock duration."""
|
||
if obj.released_at:
|
||
duration = obj.released_at - obj.locked_at
|
||
else:
|
||
duration = timezone.now() - obj.locked_at
|
||
|
||
minutes = int(duration.total_seconds() / 60)
|
||
return f"{minutes} minutes"
|
||
|
||
@display(description='Expired?')
|
||
def is_expired_display(self, obj):
|
||
"""Display if lock is expired."""
|
||
if not obj.is_active:
|
||
return 'N/A (Released)'
|
||
return 'Yes' if obj.is_expired() else 'No'
|
||
|
||
def get_queryset(self, request):
|
||
"""Optimize queryset with select_related."""
|
||
qs = super().get_queryset(request)
|
||
return qs.select_related('submission', 'locked_by')
|