Django切换邮箱为默认登录报username未知字段报错原因咨询
问题背景
使用Django自定义用户模型(将登录标识从用户名改为邮箱)时,启动Admin后台触发如下报错:
Unknown field(s) (username) specified for Profile. Check fields/fieldsets/exclude attributes of class CustomUserAdmin.
涉及的原有代码如下:
admin.py
from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .forms import UserProfileForm from .models import Profile class CustomUserAdmin(UserAdmin): add_form = UserProfileForm model = Profile list_display=('id','email','password','first_name','last_name','phone_number','is_edit','team') ordering = ('email',) pass admin.site.register(Profile, CustomUserAdmin) ##Maybe rework this later for better inclusion of all models. from django.apps import apps from django.contrib import admin class ListAdminMixin(object): def __init__(self, model, admin_site): self.list_display = [field.name for field in model._meta.fields] super(ListAdminMixin, self).__init__(model, admin_site) models = apps.get_models() for model in models: admin_class = type('AdminClass', (ListAdminMixin, admin.ModelAdmin), {}) try: admin.site.register(model, admin_class) except admin.sites.AlreadyRegistered: pass
models.py
from django.db import models from django.contrib.auth.models import AbstractUser from phonenumber_field.modelfields import PhoneNumberField import uuid from .managers import CustomUserManager class Team(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) class Profile(AbstractUser): username = None email = models.EmailField('email address', unique=True) # Here first_name = models.TextField(max_length=100, blank=True,null=True) last_name = models.TextField(max_length=100, blank=True,null=True) phone_number = PhoneNumberField(max_length=25, region='US') is_edit = models.BooleanField(default=False) team = models.ForeignKey(Team,on_delete=models.CASCADE,null=True, blank=True, default = None) USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] objects = CustomUserManager() def __str__(self): return self.email
报错根因
报错核心是父子类配置不匹配:
- 你写的
CustomUserAdmin继承了Django内置的UserAdmin,这个父类是专门给默认带username字段的用户模型设计的,它默认的fieldsets、add_fieldsets(控制后台编辑页字段分组、展示规则的核心配置)里,硬编码写死了要展示username字段。 - 但你的
Profile模型继承AbstractUser之后,直接写了username = None,把模型自带的username字段彻底移除了,还把登录标识USERNAME_FIELD改成了邮箱。 - Admin站点启动注册Profile模型的时候,会按父类UserAdmin的默认配置挨个检查字段是否在模型上真实存在,查到username时发现模型根本没有对应字段,直接抛出错误。
- 后面那段遍历所有模型自动注册的代码不会触发这个问题,因为代码里做了重复注册的异常捕获,Profile已经提前注册完成,不会走通用注册逻辑。
readonly_fields=('username',)的生效逻辑 添加这个配置能消除报错,本质是利用了Django Admin的字段校验规则,属于临时取巧的方案,并不是标准修复方式:
- Django Admin初始化ModelAdmin配置时,只会对可编辑字段做强制存在性校验:所有配置在
fields/fieldsets/add_fieldsets/list_editable里的可编辑字段,必须是模型上真实定义的字段,缺一个就会抛出字段不存在的错误。 - 一旦字段被加入
readonly_fields列表,校验逻辑就会跳过对这个字段的强校验:Admin会默认这个字段要么是模型上的只读字段,要么是开发者在ModelAdmin里自定义的展示方法/属性,不需要对应模型里的可写字段,因此不会触发username不存在的报错。 - 这种修复方式有明显副作用:你的Profile模型里根本没有username字段,后台打开用户编辑页时,原用户名的展示位置只会是空值,而且整个页面的字段分组还是Django默认给用户名登录场景设计的结构,和你现在用的邮箱登录逻辑完全不匹配。
标准修复方案
正确做法是重写CustomUserAdmin的字段配置,把所有和username相关的配置全部移除,替换为你自定义模型里的实际字段,参考代码如下:
class CustomUserAdmin(UserAdmin): add_form = UserProfileForm model = Profile list_display = ('id','email','first_name','last_name','phone_number','is_edit','team') ordering = ('email',) # 重写编辑页字段集,移除所有username相关配置 fieldsets = ( (None, {'fields': ('email', 'password')}), ('个人信息', {'fields': ('first_name', 'last_name', 'phone_number', 'team', 'is_edit')}), ('权限配置', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), ('时间记录', {'fields': ('last_login', 'date_joined')}), ) # 重写新增用户页字段集 add_fieldsets = ( (None, { 'classes': ('wide',), 'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active'), }), )
内容的提问来源于stack exchange,提问作者Arundeep Chohan
相关产品推荐
相关产品推荐

