mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:31:12 -05:00
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.
This commit is contained in:
372
django/apps/users/admin.py
Normal file
372
django/apps/users/admin.py
Normal file
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
Django admin configuration for User models.
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from unfold.admin import ModelAdmin
|
||||
from unfold.decorators import display
|
||||
from import_export import resources
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from .models import User, UserRole, UserProfile
|
||||
|
||||
|
||||
class UserResource(resources.ModelResource):
|
||||
"""Resource for importing/exporting users."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'id', 'email', 'username', 'first_name', 'last_name',
|
||||
'date_joined', 'last_login', 'is_active', 'is_staff',
|
||||
'banned', 'reputation_score', 'mfa_enabled'
|
||||
)
|
||||
export_order = fields
|
||||
|
||||
|
||||
class UserRoleInline(admin.StackedInline):
|
||||
"""Inline for user role."""
|
||||
model = UserRole
|
||||
can_delete = False
|
||||
verbose_name_plural = 'Role'
|
||||
fk_name = 'user'
|
||||
fields = ('role', 'granted_by', 'granted_at')
|
||||
readonly_fields = ('granted_at',)
|
||||
|
||||
|
||||
class UserProfileInline(admin.StackedInline):
|
||||
"""Inline for user profile."""
|
||||
model = UserProfile
|
||||
can_delete = False
|
||||
verbose_name_plural = 'Profile & Preferences'
|
||||
fk_name = 'user'
|
||||
fields = (
|
||||
('email_notifications', 'email_on_submission_approved', 'email_on_submission_rejected'),
|
||||
('profile_public', 'show_email'),
|
||||
('total_submissions', 'approved_submissions'),
|
||||
)
|
||||
readonly_fields = ('total_submissions', 'approved_submissions')
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
||||
"""Admin interface for User model."""
|
||||
|
||||
resource_class = UserResource
|
||||
|
||||
list_display = [
|
||||
'email',
|
||||
'username',
|
||||
'display_name_admin',
|
||||
'role_badge',
|
||||
'reputation_badge',
|
||||
'status_badge',
|
||||
'mfa_badge',
|
||||
'date_joined',
|
||||
'last_login',
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'banned',
|
||||
'mfa_enabled',
|
||||
'oauth_provider',
|
||||
'date_joined',
|
||||
'last_login',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'email',
|
||||
'username',
|
||||
'first_name',
|
||||
'last_name',
|
||||
]
|
||||
|
||||
ordering = ['-date_joined']
|
||||
|
||||
fieldsets = (
|
||||
('Account Information', {
|
||||
'fields': ('email', 'username', 'password')
|
||||
}),
|
||||
('Personal Information', {
|
||||
'fields': ('first_name', 'last_name', 'avatar_url', 'bio')
|
||||
}),
|
||||
('Permissions', {
|
||||
'fields': (
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
'groups',
|
||||
'user_permissions',
|
||||
)
|
||||
}),
|
||||
('Moderation', {
|
||||
'fields': (
|
||||
'banned',
|
||||
'ban_reason',
|
||||
'banned_at',
|
||||
'banned_by',
|
||||
)
|
||||
}),
|
||||
('OAuth', {
|
||||
'fields': ('oauth_provider', 'oauth_sub'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Security', {
|
||||
'fields': ('mfa_enabled', 'reputation_score'),
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('date_joined', 'last_login'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
add_fieldsets = (
|
||||
('Create New User', {
|
||||
'classes': ('wide',),
|
||||
'fields': ('email', 'username', 'password1', 'password2'),
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = [
|
||||
'date_joined',
|
||||
'last_login',
|
||||
'banned_at',
|
||||
'oauth_provider',
|
||||
'oauth_sub',
|
||||
]
|
||||
|
||||
inlines = [UserRoleInline, UserProfileInline]
|
||||
|
||||
@display(description="Name", label=True)
|
||||
def display_name_admin(self, obj):
|
||||
"""Display user's display name."""
|
||||
return obj.display_name or '-'
|
||||
|
||||
@display(description="Role", label=True)
|
||||
def role_badge(self, obj):
|
||||
"""Display user role with badge."""
|
||||
try:
|
||||
role = obj.role.role
|
||||
colors = {
|
||||
'admin': 'red',
|
||||
'moderator': 'blue',
|
||||
'user': 'green',
|
||||
}
|
||||
return format_html(
|
||||
'<span style="background-color: {}; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">{}</span>',
|
||||
colors.get(role, 'gray'),
|
||||
role.upper()
|
||||
)
|
||||
except UserRole.DoesNotExist:
|
||||
return format_html('<span style="color: gray;">No Role</span>')
|
||||
|
||||
@display(description="Reputation", label=True)
|
||||
def reputation_badge(self, obj):
|
||||
"""Display reputation score."""
|
||||
score = obj.reputation_score
|
||||
if score >= 100:
|
||||
color = 'green'
|
||||
elif score >= 50:
|
||||
color = 'blue'
|
||||
elif score >= 0:
|
||||
color = 'gray'
|
||||
else:
|
||||
color = 'red'
|
||||
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{}</span>',
|
||||
color,
|
||||
score
|
||||
)
|
||||
|
||||
@display(description="Status", label=True)
|
||||
def status_badge(self, obj):
|
||||
"""Display user status."""
|
||||
if obj.banned:
|
||||
return format_html(
|
||||
'<span style="background-color: red; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">BANNED</span>'
|
||||
)
|
||||
elif not obj.is_active:
|
||||
return format_html(
|
||||
'<span style="background-color: orange; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">INACTIVE</span>'
|
||||
)
|
||||
else:
|
||||
return format_html(
|
||||
'<span style="background-color: green; color: white; padding: 3px 8px; border-radius: 3px; font-size: 11px;">ACTIVE</span>'
|
||||
)
|
||||
|
||||
@display(description="MFA", label=True)
|
||||
def mfa_badge(self, obj):
|
||||
"""Display MFA status."""
|
||||
if obj.mfa_enabled:
|
||||
return format_html(
|
||||
'<span style="color: green;">✓ Enabled</span>'
|
||||
)
|
||||
else:
|
||||
return format_html(
|
||||
'<span style="color: gray;">✗ Disabled</span>'
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queryset with select_related."""
|
||||
qs = super().get_queryset(request)
|
||||
return qs.select_related('role', 'banned_by')
|
||||
|
||||
actions = ['ban_users', 'unban_users', 'make_moderator', 'make_user']
|
||||
|
||||
@admin.action(description="Ban selected users")
|
||||
def ban_users(self, request, queryset):
|
||||
"""Ban selected users."""
|
||||
count = 0
|
||||
for user in queryset:
|
||||
if not user.banned:
|
||||
user.ban(reason="Banned by admin", banned_by=request.user)
|
||||
count += 1
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f"{count} user(s) have been banned."
|
||||
)
|
||||
|
||||
@admin.action(description="Unban selected users")
|
||||
def unban_users(self, request, queryset):
|
||||
"""Unban selected users."""
|
||||
count = 0
|
||||
for user in queryset:
|
||||
if user.banned:
|
||||
user.unban()
|
||||
count += 1
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f"{count} user(s) have been unbanned."
|
||||
)
|
||||
|
||||
@admin.action(description="Set role to Moderator")
|
||||
def make_moderator(self, request, queryset):
|
||||
"""Set users' role to moderator."""
|
||||
from .services import RoleService
|
||||
|
||||
count = 0
|
||||
for user in queryset:
|
||||
RoleService.assign_role(user, 'moderator', request.user)
|
||||
count += 1
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f"{count} user(s) have been set to Moderator role."
|
||||
)
|
||||
|
||||
@admin.action(description="Set role to User")
|
||||
def make_user(self, request, queryset):
|
||||
"""Set users' role to user."""
|
||||
from .services import RoleService
|
||||
|
||||
count = 0
|
||||
for user in queryset:
|
||||
RoleService.assign_role(user, 'user', request.user)
|
||||
count += 1
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f"{count} user(s) have been set to User role."
|
||||
)
|
||||
|
||||
|
||||
@admin.register(UserRole)
|
||||
class UserRoleAdmin(ModelAdmin):
|
||||
"""Admin interface for UserRole model."""
|
||||
|
||||
list_display = ['user', 'role', 'is_moderator', 'is_admin', 'granted_at', 'granted_by']
|
||||
list_filter = ['role', 'granted_at']
|
||||
search_fields = ['user__email', 'user__username']
|
||||
ordering = ['-granted_at']
|
||||
|
||||
readonly_fields = ['granted_at']
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queryset."""
|
||||
qs = super().get_queryset(request)
|
||||
return qs.select_related('user', 'granted_by')
|
||||
|
||||
|
||||
@admin.register(UserProfile)
|
||||
class UserProfileAdmin(ModelAdmin):
|
||||
"""Admin interface for UserProfile model."""
|
||||
|
||||
list_display = [
|
||||
'user',
|
||||
'total_submissions',
|
||||
'approved_submissions',
|
||||
'approval_rate',
|
||||
'email_notifications',
|
||||
'profile_public',
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'email_notifications',
|
||||
'profile_public',
|
||||
'show_email',
|
||||
]
|
||||
|
||||
search_fields = ['user__email', 'user__username']
|
||||
|
||||
readonly_fields = ['created', 'modified', 'total_submissions', 'approved_submissions']
|
||||
|
||||
fieldsets = (
|
||||
('User', {
|
||||
'fields': ('user',)
|
||||
}),
|
||||
('Statistics', {
|
||||
'fields': ('total_submissions', 'approved_submissions'),
|
||||
}),
|
||||
('Notification Preferences', {
|
||||
'fields': (
|
||||
'email_notifications',
|
||||
'email_on_submission_approved',
|
||||
'email_on_submission_rejected',
|
||||
)
|
||||
}),
|
||||
('Privacy Settings', {
|
||||
'fields': ('profile_public', 'show_email'),
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created', 'modified'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
@display(description="Approval Rate")
|
||||
def approval_rate(self, obj):
|
||||
"""Display approval rate percentage."""
|
||||
if obj.total_submissions == 0:
|
||||
return '-'
|
||||
|
||||
rate = (obj.approved_submissions / obj.total_submissions) * 100
|
||||
|
||||
if rate >= 80:
|
||||
color = 'green'
|
||||
elif rate >= 60:
|
||||
color = 'blue'
|
||||
elif rate >= 40:
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'red'
|
||||
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{:.1f}%</span>',
|
||||
color,
|
||||
rate
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queryset."""
|
||||
qs = super().get_queryset(request)
|
||||
return qs.select_related('user')
|
||||
Reference in New Issue
Block a user