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:
pacnpal
2025-11-08 15:34:04 -05:00
parent 9c46ef8b03
commit d6ff4cc3a3
335 changed files with 61926 additions and 73 deletions

372
django/apps/users/admin.py Normal file
View 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')