Django自定义多用户模型在admin创建时生成随机密码报NoneType无save方法错误
问题根因定位
报AttributeError: 'NoneType' object has no attribute 'save'的核心触发逻辑全部来自Django多表用户继承场景下的配置错位,无其他复杂原因:
- 自定义多表继承的用户子类时,父子模型的OneToOne关联字段没有配置正确的
parent_link和related_name,导致admin表单保存流程中,从表单实例化返回的关联对象为空,拿到None值 - admin自定义保存逻辑时,跳过了ModelForm本身的保存流程,手动实例化了一个空对象并调用其save方法,而非使用表单校验通过后生成的有效用户实例
- post_save信号触发时,没有区分用户类型就直接查询关联的子类对象,匹配到非目标类型用户时查询结果为None,后续代码链中对这个None值调用save方法直接抛出报错
可运行实现方案
1. models.py 核心逻辑实现
from django.db import models from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager from django.conf import settings # 自定义用户管理器,内置随机密码生成逻辑 class CustomUserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): if not email: raise ValueError("用户必须绑定有效邮箱") email = self.normalize_email(email) user = self.model(email=email, **extra_fields) # 自动生成12位随机密码,替代管理员手动输入 random_password = self.make_random_password(length=12) user.set_password(random_password) user.save(using=self._db) # 将生成的密码挂载到实例属性,供后续发邮件逻辑读取 user._generated_pwd = random_password return user def create_superuser(self, email, password=None, **extra_fields): extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("is_active", True) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user # 基础用户模型 class BaseUser(AbstractBaseUser, PermissionsMixin): email = models.EmailField(unique=True, verbose_name="登录邮箱") name = models.CharField(max_length=50, verbose_name="用户姓名") is_staff = models.BooleanField(default=False, verbose_name="是否可登录后台") is_active = models.BooleanField(default=True, verbose_name="账号是否激活") USER_TYPE_CHOICES = ( ("admin", "系统管理员"), ("employee", "员工"), ("relative", "家属") ) user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES, editable=False, default="") objects = CustomUserManager() USERNAME_FIELD = "email" REQUIRED_FIELDS = ["name"] def save(self, *args, **kwargs): # 保存时自动识别用户类型,解决关联对象取不到的问题 if not self.user_type: if isinstance(self, Admin): self.user_type = "admin" elif isinstance(self, Employee): self.user_type = "employee" elif isinstance(self, Relative): self.user_type = "relative" super().save(*args, **kwargs) # 三类用户子类 class Admin(BaseUser): class Meta: verbose_name = "系统管理员" # 配置关联参数,解决反向查询冲突 baseuser_ptr = models.OneToOneField( BaseUser, on_delete=models.CASCADE, parent_link=True, related_name="admin_profile" ) class Employee(BaseUser): department = models.CharField(max_length=50, verbose_name="所属部门") job_number = models.CharField(max_length=20, unique=True, verbose_name="工号") class Meta: verbose_name = "员工" baseuser_ptr = models.OneToOneField( BaseUser, on_delete=models.CASCADE, parent_link=True, related_name="employee_profile" ) class Relative(BaseUser): relation = models.CharField(max_length=30, verbose_name="与员工关系") employee = models.ForeignKey(Employee, on_delete=models.CASCADE, verbose_name="关联员工") class Meta: verbose_name = "家属" baseuser_ptr = models.OneToOneField( BaseUser, on_delete=models.CASCADE, parent_link=True, related_name="relative_profile" )
2. post_save 邮件信号实现
在同app的signals.py文件中编写逻辑,避免循环导入:
from django.db.models.signals import post_save from django.dispatch import receiver from django.core.mail import send_mail from django.conf import settings from .models import BaseUser @receiver(post_save, sender=BaseUser) def send_account_email(sender, instance, created, **kwargs): # 仅触发新建普通用户的场景,跳过管理员账号 if not created or instance.user_type == "admin": return # 校验是否存在生成的随机密码,无值则跳过避免重复发信 generated_pwd = getattr(instance, "_generated_pwd", None) if not generated_pwd: return # 按用户类型匹配邮件模板 if instance.user_type == "employee": subject = "您的员工账号已开通" content = f"您好{instance.name},您的员工账号已创建,登录邮箱为{instance.email},初始密码为{generated_pwd},请及时登录修改。" elif instance.user_type == "relative": subject = "您的家属关联账号已开通" content = f"您好{instance.name},您的家属账号已创建,登录邮箱为{instance.email},初始密码为{generated_pwd},请及时登录修改。" else: return send_mail( subject=subject, message=content, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[instance.email], fail_silently=False )
3. admin.py 后台配置实现
from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import BaseUser, Admin, Employee, Relative class BaseUserAdmin(UserAdmin): # 隐藏密码输入框,无需管理员手动填写 exclude = ("password", ) list_display = ("email", "name", "user_type", "is_active") # 后台编辑页字段配置,移除默认的密码修改入口 fieldsets = ( (None, {"fields": ("email", "name", "is_active")}), ("权限配置", {"fields": ("groups", "user_permissions")}), ) add_fieldsets = ( (None, { "classes": ("wide",), "fields": ("email", "name"), }), ) ordering = ("email", ) # 为每个用户子类单独绑定admin类,确保表单保存时生成有效子类实例 @admin.register(Employee) class EmployeeAdmin(BaseUserAdmin): fieldsets = BaseUserAdmin.fieldsets + ( ("员工信息", {"fields": ("department", "job_number")}), ) add_fieldsets = BaseUserAdmin.add_fieldsets + ( ("员工信息", {"fields": ("department", "job_number")}), ) @admin.register(Relative) class RelativeAdmin(BaseUserAdmin): fieldsets = BaseUserAdmin.fieldsets + ( ("家属信息", {"fields": ("relation", "employee")}), ) add_fieldsets = BaseUserAdmin.add_fieldsets + ( ("家属信息", {"fields": ("relation", "employee")}), ) # 注册管理员模型 admin.site.register(Admin, BaseUserAdmin)
关键配置注意事项
- 项目
settings.py中必须指定AUTH_USER_MODEL = "你的app名称.BaseUser",否则自定义用户模型不生效,关联关系全部失效 - 信号注册需在app的
apps.py的ready方法中导入signals.py,否则信号逻辑不会被触发 - 核心修复点:不要在admin的自定义保存逻辑中手动实例化空对象调用save,全部依赖ModelForm的save方法生成有效实例,从根源上避免None值报错
内容的提问来源于stack exchange,提问作者VISHESH MANGLA
相关产品推荐
相关产品推荐

