Files
thrilltrack-explorer/django/apps/moderation/admin.py
pacnpal d6ff4cc3a3 Add email templates for user notifications and account management
- 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.
2025-11-08 15:34:04 -05:00

425 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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')