From 679de16e4ffe5cfa2a9d8fee0413a4dc8e733fb3 Mon Sep 17 00:00:00 2001
From: pacnpal <183241239+pacnpal@users.noreply.github.com>
Date: Sat, 27 Sep 2025 11:59:29 -0400
Subject: [PATCH] Refactor account adapters and admin classes; enhance type
hinting for better clarity and maintainability, ensuring consistent typing
across methods and improving overall code quality.
---
apps/accounts/adapters.py | 75 ++++++++++++++++-------
apps/accounts/admin.py | 123 ++++++++++++++++++++------------------
2 files changed, 119 insertions(+), 79 deletions(-)
diff --git a/apps/accounts/adapters.py b/apps/accounts/adapters.py
index 3b2a79b4..bbbf7350 100644
--- a/apps/accounts/adapters.py
+++ b/apps/accounts/adapters.py
@@ -1,64 +1,95 @@
from django.conf import settings
-from allauth.account.adapter import DefaultAccountAdapter
-from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
+from django.http import HttpRequest
+from typing import Optional, Any, Dict, Literal, TYPE_CHECKING, cast
+from allauth.account.adapter import DefaultAccountAdapter # type: ignore[import]
+from allauth.account.models import EmailConfirmation, EmailAddress # type: ignore[import]
+from allauth.socialaccount.adapter import DefaultSocialAccountAdapter # type: ignore[import]
+from allauth.socialaccount.models import SocialLogin # type: ignore[import]
from django.contrib.auth import get_user_model
from django.contrib.sites.shortcuts import get_current_site
+if TYPE_CHECKING:
+ from django.contrib.auth.models import AbstractUser
+
User = get_user_model()
class CustomAccountAdapter(DefaultAccountAdapter):
- def is_open_for_signup(self, request):
+ def is_open_for_signup(self, request: HttpRequest) -> Literal[True]:
"""
Whether to allow sign ups.
"""
return True
- def get_email_confirmation_url(self, request, emailconfirmation):
+ def get_email_confirmation_url(self, request: HttpRequest, emailconfirmation: EmailConfirmation) -> str:
"""
Constructs the email confirmation (activation) url.
"""
get_current_site(request)
- return f"{settings.LOGIN_REDIRECT_URL}verify-email?key={emailconfirmation.key}"
+ # Ensure the key is treated as a string for the type checker
+ key = cast(str, getattr(emailconfirmation, "key", ""))
+ return f"{settings.LOGIN_REDIRECT_URL}verify-email?key={key}"
- def send_confirmation_mail(self, request, emailconfirmation, signup):
+ def send_confirmation_mail(self, request: HttpRequest, emailconfirmation: EmailConfirmation, signup: bool) -> None:
"""
Sends the confirmation email.
"""
current_site = get_current_site(request)
activate_url = self.get_email_confirmation_url(request, emailconfirmation)
- ctx = {
- "user": emailconfirmation.email_address.user,
- "activate_url": activate_url,
- "current_site": current_site,
- "key": emailconfirmation.key,
- }
+ # Cast key to str for typing consistency and template context
+ key = cast(str, getattr(emailconfirmation, "key", ""))
+
+ # Determine template early
if signup:
email_template = "account/email/email_confirmation_signup"
else:
email_template = "account/email/email_confirmation"
- self.send_mail(email_template, emailconfirmation.email_address.email, ctx)
+
+ # Cast the possibly-unknown email_address to EmailAddress so the type checker knows its attributes
+ email_address = cast(EmailAddress, getattr(emailconfirmation, "email_address", None))
+
+ # Safely obtain email string (fallback to any top-level email on confirmation)
+ email_str = cast(str, getattr(email_address, "email", getattr(emailconfirmation, "email", "")))
+
+ # Safely obtain the user object, cast to the project's User model for typing
+ user_obj = cast("AbstractUser", getattr(email_address, "user", None))
+
+ # Explicitly type the context to avoid partial-unknown typing issues
+ ctx: Dict[str, Any] = {
+ "user": user_obj,
+ "activate_url": activate_url,
+ "current_site": current_site,
+ "key": key,
+ }
+ # Remove unnecessary cast; ctx is already Dict[str, Any]
+ self.send_mail(email_template, email_str, ctx) # type: ignore
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
- def is_open_for_signup(self, request, sociallogin):
+ def is_open_for_signup(self, request: HttpRequest, sociallogin: SocialLogin) -> Literal[True]:
"""
Whether to allow social account sign ups.
"""
return True
- def populate_user(self, request, sociallogin, data):
+ def populate_user(
+ self, request: HttpRequest, sociallogin: SocialLogin, data: Dict[str, Any]
+ ) -> "AbstractUser": # type: ignore[override]
"""
Hook that can be used to further populate the user instance.
"""
- user = super().populate_user(request, sociallogin, data)
- if sociallogin.account.provider == "discord":
- user.discord_id = sociallogin.account.uid
- return user
+ user = super().populate_user(request, sociallogin, data) # type: ignore
+ if getattr(sociallogin.account, "provider", None) == "discord": # type: ignore
+ user.discord_id = getattr(sociallogin.account, "uid", None) # type: ignore
+ return cast("AbstractUser", user) # Ensure return type is explicit
- def save_user(self, request, sociallogin, form=None):
+ def save_user(
+ self, request: HttpRequest, sociallogin: SocialLogin, form: Optional[Any] = None
+ ) -> "AbstractUser": # type: ignore[override]
"""
Save the newly signed up social login.
"""
- user = super().save_user(request, sociallogin, form)
- return user
+ user = super().save_user(request, sociallogin, form) # type: ignore
+ if user is None:
+ raise ValueError("User creation failed")
+ return cast("AbstractUser", user) # Ensure return type is explicit
diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py
index 3929fc70..5d92c4b2 100644
--- a/apps/accounts/admin.py
+++ b/apps/accounts/admin.py
@@ -1,7 +1,10 @@
+from typing import Any
from django.contrib import admin
-from django.contrib.auth.admin import UserAdmin
+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,
@@ -12,7 +15,7 @@ from .models import (
)
-class UserProfileInline(admin.StackedInline):
+class UserProfileInline(admin.StackedInline[UserProfile, admin.options.AdminSite]):
model = UserProfile
can_delete = False
verbose_name_plural = "Profile"
@@ -39,7 +42,7 @@ class UserProfileInline(admin.StackedInline):
)
-class TopListItemInline(admin.TabularInline):
+class TopListItemInline(admin.TabularInline[TopListItem]):
model = TopListItem
extra = 1
fields = ("content_type", "object_id", "rank", "notes")
@@ -47,7 +50,7 @@ class TopListItemInline(admin.TabularInline):
@admin.register(User)
-class CustomUserAdmin(UserAdmin):
+class CustomUserAdmin(DjangoUserAdmin[User]):
list_display = (
"username",
"email",
@@ -74,7 +77,7 @@ class CustomUserAdmin(UserAdmin):
"ban_users",
"unban_users",
]
- inlines = [UserProfileInline]
+ inlines: list[type[admin.StackedInline[UserProfile]]] = [UserProfileInline]
fieldsets = (
(None, {"fields": ("username", "password")}),
@@ -126,75 +129,82 @@ class CustomUserAdmin(UserAdmin):
)
@admin.display(description="Avatar")
- def get_avatar(self, obj):
- if obj.profile.avatar:
+ def get_avatar(self, obj: User) -> str:
+ profile = getattr(obj, "profile", None)
+ if profile and getattr(profile, "avatar", None):
return format_html(
- '',
- obj.profile.avatar.url,
+ '
',
+ getattr(profile.avatar, "url", ""), # type: ignore
)
return format_html(
'