Django自定义用户创建超级用户时遇非空字段错误求助
问题
使用Django自定义User模型时,执行python manage.py createsuperuser创建超级用户,反复报错:
CommandError: This field cannot be blank. This field cannot be null.
诡异的是,重复用同一邮箱创建时会提示邮箱已被占用,但登录时却显示用户无profile。相关代码如下:
User模型代码
from typing import ClassVar from django.utils import timezone from django.contrib.auth.models import AbstractBaseUser,PermissionsMixin, Group, Permission from django.db import models from django.db.models import BooleanField from django.db.models import CharField from django.db.models import EmailField from django.urls import reverse from django.utils.translation import gettext_lazy as _ from rhs_soccer.users.enums import UserRoles from .managers import UserManager class User(AbstractBaseUser, PermissionsMixin): """ Default custom user model for RHS Boys Soccer Club. If adding fields that need to be filled at user signup, check forms.SignupForm and forms.SocialSignupForms accordingly. """ # First and last name do not cover name patterns around the globe #id = models.UUIDField(default=uuid.uuid4, primary_key = True, editable=False, unique=True) first_name = CharField(_("first name"), max_length=30, blank = True, null = True) last_name = CharField(_("last name"), max_length=30, blank = True, null = True) email = EmailField(_("email address"), unique=True) role = CharField( _("Role"), max_length=50, choices=UserRoles.choices(), default=UserRoles.PLAYER.value, ) username = None # type: ignore[assignment] is_active = BooleanField(_("active"), default=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) groups = models.ManyToManyField( Group, related_name="user_set", related_query_name="user", blank=True, ) user_permissions = models.ManyToManyField( Permission, related_name="user_set", related_query_name="user", blank=True, ) date_joined = models.DateTimeField(default=timezone.now) USERNAME_FIELD = "email" REQUIRED_FIELDS = [] objects: ClassVar[UserManager] = UserManager() def get_absolute_url(self) -> str: """Get URL for user's detail view. Returns: str: URL for user detail. """ return reverse("users:detail", kwargs={"pk": self.id}) def __str__(self) -> str: return self.email def get_full_name(self) -> str: return f"{self.first_name} {self.last_name}"
UserManager代码
from typing import TYPE_CHECKING from django.contrib.auth.hashers import make_password from django.contrib.auth.models import BaseUserManager from django.utils import timezone # from django.contrib.auth.models import UserManager as DjangoUserManager if TYPE_CHECKING: from .models import User # noqa: F401 class UserManager(BaseUserManager): """Custom manager for the User model.""" def _create_user(self, email: str, password: str | None, **extra_fields): """ Create and save a user with the given email and password. """ if not email: msg = "The given email must be set" raise ValueError(msg) email = self.normalize_email(email) extra_fields.setdefault('date_joined', timezone.now()) user = self.model(email=email, **extra_fields) user.password = make_password(password) user.save(using=self._db) return user def create_user(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", False) return self._create_user(email, password, **extra_fields) def create_player(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) extra_fields.setdefault("role", "PLAYER") return self._create_user(email, password, **extra_fields) def create_coach(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) extra_fields.setdefault("role", "COACH") return self._create_user(email, password, **extra_fields) def create_manager(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) extra_fields.setdefault("role", "MANAGER") return self._create_user(email, password, **extra_fields) def create_parent(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) extra_fields.setdefault("role", "PARENT") return self._create_user(email, password, **extra_fields) def create_admin(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("role", "ADMIN") return self._create_user(email, password, **extra_fields) def create_superuser(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("role", "ADMIN") if 'first_name' not in extra_fields: extra_fields['first_name'] = '' if 'last_name' not in extra_fields: extra_fields['last_name'] = '' if extra_fields.get("is_staff") is not True: msg = "Superuser must have is_staff=True." raise ValueError(msg) if extra_fields.get("is_superuser") is not True: msg = "Superuser must have is_superuser=True." raise ValueError(msg) return self._create_user(email, password, **extra_fields)
forms.py代码
from allauth.account.forms import SignupForm from allauth.socialaccount.forms import SignupForm as SocialSignupForm from django.contrib.auth import forms as admin_forms from django.forms import EmailField from django.utils.translation import gettext_lazy as _ from django import forms from .models import User class UserAdminChangeForm(admin_forms.UserChangeForm): class Meta(admin_forms.UserChangeForm.Meta): # type: ignore[name-defined] model = User fields = ('email', 'first_name', 'last_name', 'role', 'is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions') field_classes = {"email": EmailField} class UserAdminCreationForm(admin_forms.UserCreationForm): """ Form for User Creation in the Admin Area. To change user signup, see UserSignupForm and UserSocialSignupForm. """ first_name = forms.CharField(required=False) last_name = forms.CharField(required=False) class Meta(admin_forms.UserCreationForm.Meta): # type: ignore[name-defined] model = User fields = ("email","first_name", "last_name", "role", "password1", "password2") field_classes = {"email": EmailField} error_messages = { "email": {"unique": _("This email has already been taken.")}, } class UserSignupForm(SignupForm): """ Form that will be rendered on a user sign up section/screen. Default fields will be added automatically. Check UserSocialSignupForm for accounts created from social. """ class UserSocialSignupForm(SocialSignupForm): """ Renders the form when user has signed up using social accounts. Default fields will be added automatically. See UserSignupForm otherwise. """
admin.py代码
from allauth.account.decorators import secure_admin_login from django.conf import settings from django.contrib import admin #from django.contrib.auth import admin as auth_admin from django.utils.translation import gettext_lazy as _ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from .forms import UserAdminChangeForm from .forms import UserAdminCreationForm from .models import User if settings.DJANGO_ADMIN_FORCE_ALLAUTH: # Force the `admin` sign in process to go through the `django-allauth` workflow: # https://docs.allauth.org/en/latest/common/admin.html#admin admin.autodiscover() admin.site.login = secure_admin_login(admin.site.login) # type: ignore[method-assign] @admin.register(User) class UserAdmin(BaseUserAdmin): form = UserAdminChangeForm add_form = UserAdminCreationForm fieldsets = ( (None, {"fields": ("email", "role", "password")}), ( _("Personal info"), { "fields": ( "first_name", "last_name", ), }, ), ( _("Permissions"), { "fields": ( "is_active", "is_staff", "is_superuser", "groups", "user_permissions", ), }, ), (_("Important dates"), {"fields": ("last_login", "date_joined")}), ) list_display = ["email", "first_name", "last_name", "role"] search_fields = [ "first_name", "last_name", "email" ] list_editable = ["role"] list_filter = ["role"] ordering = ["id"] add_fieldsets = ( ( None, { "classes": ("wide",), "fields": ("email", "role", "password1", "password2", "first_name", "last_name"), }, ), )
排查思路与解决方案
定位具体报错字段
错误信息没明确字段名,可临时修改_create_user方法,在user.save()前打印所有字段值,或者打开Django调试模式查看完整错误栈;也可以直接查看数据库中User表的字段约束,确认是否有遗漏的非空设置。另外,把create_superuser里的first_name和last_name赋值改为None(模型允许null=True,空字符串和None是不同的数据库值),测试是否解决问题。验证迁移文件的正确性
虽然模型里first_name和last_name设置了blank=True, null=True,但要检查migrations文件夹里的迁移文件,确认这些字段的约束是否同步到了数据库。如果迁移有问题,执行python manage.py makemigrations重新生成迁移,再用migrate更新数据库。绕开命令行表单直接测试创建逻辑
用Python Shell直接调用Manager方法创建超级用户,排除表单验证的干扰:python manage.py shell from users.models import User User.objects.create_superuser(email='admin@test.com', password='yourpass123')如果这样能成功,说明问题出在
createsuperuser命令使用的表单上,可检查UserAdminCreationForm的验证逻辑是否有隐性要求。清理不完整的用户数据
因为报错时用户已经被部分创建(邮箱已存在),但数据不完整导致无法登录。直接进入数据库删除该条记录,或者用Shell补全字段值:user = User.objects.get(email='admin@test.com') user.first_name = 'Admin' user.last_name = 'User' user.save()检查PermissionsMixin的字段冲突
虽然手动重定义了groups和user_permissions,但可暂时注释掉这两个自定义字段,用PermissionsMixin默认的字段测试,排除关联字段的隐性约束问题。
内容的提问来源于stack exchange,提问作者Wanjiru

