from typing import Any from django.contrib import admin from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin from django.utils.html import format_html from django.contrib.auth.models import Group from django.http import HttpRequest from django.db.models import QuerySet from .models import ( User, UserProfile, EmailVerification, PasswordReset, TopList, TopListItem, ) class UserProfileInline(admin.StackedInline[UserProfile, admin.options.AdminSite]): model = UserProfile can_delete = False verbose_name_plural = "Profile" fieldsets = ( ( "Personal Info", {"fields": ("display_name", "avatar", "pronouns", "bio")}, ), ( "Social Media", {"fields": ("twitter", "instagram", "youtube", "discord")}, ), ( "Ride Credits", { "fields": ( "coaster_credits", "dark_ride_credits", "flat_ride_credits", "water_ride_credits", ) }, ), ) class TopListItemInline(admin.TabularInline[TopListItem]): model = TopListItem extra = 1 fields = ("content_type", "object_id", "rank", "notes") ordering = ("rank",) @admin.register(User) class CustomUserAdmin(DjangoUserAdmin[User]): list_display = ( "username", "email", "get_avatar", "get_status", "role", "date_joined", "last_login", "get_credits", ) list_filter = ( "is_active", "is_staff", "role", "is_banned", "groups", "date_joined", ) search_fields = ("username", "email") ordering = ("-date_joined",) actions = [ "activate_users", "deactivate_users", "ban_users", "unban_users", ] inlines: list[type[admin.StackedInline[UserProfile]]] = [UserProfileInline] fieldsets = ( (None, {"fields": ("username", "password")}), ("Personal info", {"fields": ("email", "pending_email")}), ( "Roles and Permissions", { "fields": ("role", "groups", "user_permissions"), "description": ( "Role determines group membership. Groups determine permissions." ), }, ), ( "Status", { "fields": ("is_active", "is_staff", "is_superuser"), "description": "These are automatically managed based on role.", }, ), ( "Ban Status", { "fields": ("is_banned", "ban_reason", "ban_date"), }, ), ( "Preferences", { "fields": ("theme_preference",), }, ), ("Important dates", {"fields": ("last_login", "date_joined")}), ) add_fieldsets = ( ( None, { "classes": ("wide",), "fields": ( "username", "email", "password1", "password2", "role", ), }, ), ) @admin.display(description="Avatar") def get_avatar(self, obj: User) -> str: profile = getattr(obj, "profile", None) if profile and getattr(profile, "avatar", None): return format_html( '', getattr(profile.avatar, "url", ""), # type: ignore ) return format_html( '
{0}
', getattr(obj, "username", "?")[0].upper(), # type: ignore ) @admin.display(description="Status") def get_status(self, obj: User) -> str: if getattr(obj, "is_banned", False): return format_html('{}', "Banned") if not getattr(obj, "is_active", True): return format_html('{}', "Inactive") if getattr(obj, "is_superuser", False): return format_html('{}', "Superuser") if getattr(obj, "is_staff", False): return format_html('{}', "Staff") return format_html('{}', "Active") @admin.display(description="Ride Credits") def get_credits(self, obj: User) -> str: try: profile = getattr(obj, "profile", None) if not profile: return "-" return format_html( "RC: {0}
DR: {1}
FR: {2}
WR: {3}", getattr(profile, "coaster_credits", 0), getattr(profile, "dark_ride_credits", 0), getattr(profile, "flat_ride_credits", 0), getattr(profile, "water_ride_credits", 0), ) except UserProfile.DoesNotExist: return "-" @admin.action(description="Activate selected users") def activate_users(self, request: HttpRequest, queryset: QuerySet[User]) -> None: queryset.update(is_active=True) @admin.action(description="Deactivate selected users") def deactivate_users(self, request: HttpRequest, queryset: QuerySet[User]) -> None: queryset.update(is_active=False) @admin.action(description="Ban selected users") def ban_users(self, request: HttpRequest, queryset: QuerySet[User]) -> None: from django.utils import timezone queryset.update(is_banned=True, ban_date=timezone.now()) @admin.action(description="Unban selected users") def unban_users(self, request: HttpRequest, queryset: QuerySet[User]) -> None: queryset.update(is_banned=False, ban_date=None, ban_reason="") def save_model( self, request: HttpRequest, obj: User, form: Any, change: bool ) -> None: creating = not obj.pk super().save_model(request, obj, form, change) if creating and getattr(obj, "role", "USER") != "USER": group = Group.objects.filter(name=getattr(obj, "role", None)).first() if group: obj.groups.add(group) # type: ignore[attr-defined] @admin.register(UserProfile) class UserProfileAdmin(admin.ModelAdmin[UserProfile]): list_display = ( "user", "display_name", "coaster_credits", "dark_ride_credits", "flat_ride_credits", "water_ride_credits", ) list_filter = ( "coaster_credits", "dark_ride_credits", "flat_ride_credits", "water_ride_credits", ) search_fields = ("user__username", "user__email", "display_name", "bio") fieldsets = ( ( "User Information", {"fields": ("user", "display_name", "avatar", "pronouns", "bio")}, ), ( "Social Media", {"fields": ("twitter", "instagram", "youtube", "discord")}, ), ( "Ride Credits", { "fields": ( "coaster_credits", "dark_ride_credits", "flat_ride_credits", "water_ride_credits", ) }, ), ) @admin.register(EmailVerification) class EmailVerificationAdmin(admin.ModelAdmin[EmailVerification]): list_display = ("user", "created_at", "last_sent", "is_expired") list_filter = ("created_at", "last_sent") search_fields = ("user__username", "user__email", "token") readonly_fields = ("created_at", "last_sent") fieldsets = ( ("Verification Details", {"fields": ("user", "token")}), ("Timing", {"fields": ("created_at", "last_sent")}), ) @admin.display(description="Status") def is_expired(self, obj: EmailVerification) -> str: from django.utils import timezone from datetime import timedelta if timezone.now() - getattr(obj, "last_sent", timezone.now()) > timedelta(days=1): return format_html('{}', "Expired") return format_html('{}', "Valid") @admin.register(TopList) class TopListAdmin(admin.ModelAdmin[TopList]): list_display = ("title", "user", "category", "created_at", "updated_at") list_filter = ("category", "created_at", "updated_at") search_fields = ("title", "user__username", "description") inlines: list[type[admin.TabularInline[TopListItem]]] = [TopListItemInline] fieldsets = ( ( "Basic Information", {"fields": ("user", "title", "category", "description")}, ), ( "Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}, ), ) readonly_fields = ("created_at", "updated_at") @admin.register(TopListItem) class TopListItemAdmin(admin.ModelAdmin[TopListItem]): list_display = ("top_list", "content_type", "object_id", "rank") list_filter = ("top_list__category", "rank") search_fields = ("top_list__title", "notes") ordering = ("top_list", "rank") fieldsets = ( ("List Information", {"fields": ("top_list", "rank")}), ("Item Details", {"fields": ("content_type", "object_id", "notes")}), ) @admin.register(PasswordReset) class PasswordResetAdmin(admin.ModelAdmin[PasswordReset]): """Admin interface for password reset tokens""" list_display = ( "user", "created_at", "expires_at", "is_expired", "used", ) list_filter = ( "used", "created_at", "expires_at", ) search_fields = ( "user__username", "user__email", "token", ) readonly_fields = ( "token", "created_at", "expires_at", ) date_hierarchy = "created_at" ordering = ("-created_at",) fieldsets = ( ( "Reset Details", { "fields": ( "user", "token", "used", ) }, ), ( "Timing", { "fields": ( "created_at", "expires_at", ) }, ), ) @admin.display(description="Status", boolean=True) def is_expired(self, obj: PasswordReset) -> str: from django.utils import timezone if getattr(obj, "used", False): return format_html('{}', "Used") elif timezone.now() > getattr(obj, "expires_at", timezone.now()): return format_html('{}', "Expired") return format_html('{}', "Valid") def has_add_permission(self, request: HttpRequest) -> bool: """Disable manual creation of password reset tokens""" return False def has_change_permission(self, request: HttpRequest, obj: Any = None) -> bool: """Allow viewing but restrict editing of password reset tokens""" return getattr(request.user, "is_superuser", False)