Django - 允许在大型用户模型中重复用户的电子邮件,但在其扩展模型中是唯一的?
我正在 Django 中开发一个项目,该项目有一个从 AbstractBaseUser 扩展的用户模型; 2 个模型从用户(客户和技术人员)延伸。创建用户、客户或技术人员时一切正常。
但是,每当我使用同一电子邮件(因为我将电子邮件定义为用户名 USERNAME_FIELD)将新用户注册为客户或技术人员(/api/customer/register/ 或 /api/customer/register/)时,它都会返回“该电子邮件已存在。”当然,我理解这一点,因为我在用户模型中定义了用户名和电子邮件字段的唯一约束。 我希望客户在自己的模型中是唯一的,但在用户模型中不是唯一的,对于技术人员来说也是如此。就像当您注册 Uber 或 DoorDash 时,您要么想成为客户,要么想成为司机,或者两者都可以。并且您可以使用相同的电子邮件地址来注册这两个帐户。但是,如果您已经使用该电子邮件地址作为客户或司机注册,则不能使用相同的电子邮件地址再次注册
我尝试过对此进行调整,但到目前为止还没有任何效果!我的问题是,解决这个问题的最佳方法是什么?我应该为客户和技术人员创建 2 个不同的表吗?如果有人知道 Uber、DoorDash 或 Lyft 使用什么架构来解决这个问题?
非常感谢您的帮助!我将在这里发布我的 models.py 供大家查看。干杯!
import uuid
from typing import Any, Optional
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
from django.db import models
from rest_framework_simplejwt.tokens import RefreshToken
class UserManager(BaseUserManager): # type: ignore
"""UserManager class."""
# type: ignore
def create_user(self, username: str, email: str, password: Optional[str] = None) -> 'User':
"""Create and return a `User` with an email, username and password."""
if username is None:
raise TypeError('Users must have a username.')
if email is None:
raise TypeError('Users must have an email address.')
user = self.model(username=username, email=self.normalize_email(email))
user.set_password(password)
user.save()
return user
def create_customer(self, username: str, email: str, password: Optional[str] = None) -> 'Customer':
"""Create and return a `User` with an email, username and password."""
if username is None:
raise TypeError('Users must have a username.')
if email is None:
raise TypeError('Users must have an email address.')
user = user.model(username=username, email=self.normalize_email(email))
user.set_password(password)
user.save()
return user
"""def create_customer(self, username, email, password=None, **extra_fields):
user = self.create_user(username, email, password, **extra_fields)
Customer.objects.create(user=user)
return user"""
def create_technician(self, username, email, password=None, **extra_fields):
user = self.create_user(username, email, password, **extra_fields)
Technician.objects.create(user=user)
return user
def create_superuser(self, username: str, email: str, password: str) -> 'User': # type: ignore
"""Create and return a `User` with superuser (admin) permissions."""
if password is None:
raise TypeError('Superusers must have a password.')
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.is_active = True
user.save()
return user
class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
username = models.CharField(db_index=True, max_length=255, unique=True)
email = models.EmailField(db_index=True, unique=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
#bio = models.TextField(null=True)
full_name = models.CharField(max_length=20000, null=True)
birth_date = models.DateField(null=True)
is_customer = models.BooleanField(default=False)
is_technician = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
# Tells Django that the UserManager class defined above should manage
# objects of this type.
objects = UserManager()
def __str__(self) -> str:
"""Return a string representation of this `User`."""
string = self.email if self.email != '' else self.get_full_name()
return f'{self.id} {string}'
@property
def tokens(self) -> dict[str, str]:
"""Allow us to get a user's token by calling `user.tokens`."""
refresh = RefreshToken.for_user(self)
return {'refresh': str(refresh), 'access': str(refresh.access_token)}
def get_full_name(self) -> Optional[str]:
"""Return the full name of the user."""
return self.full_name
def get_short_name(self) -> str:
"""Return user username."""
return self.username
class Customer(models.Model):
user = models.OneToOneField(User, related_name="customer", on_delete=models.CASCADE, primary_key=True)
objects = UserManager()
class Technician(models.Model):
user = models.OneToOneField(User, related_name="technician", on_delete=models.CASCADE, primary_key=True)
我的序列化器.py
from django.contrib.auth import authenticate
from rest_framework import exceptions, serializers
from rest_framework_simplejwt.tokens import RefreshToken, TokenError
from .models import Customer, Technician
from django.contrib.auth import get_user_model
User = get_user_model()
from .utils import validate_email as email_is_valid
class RegistrationSerializer(serializers.ModelSerializer[User]):
"""Serializers registration requests and creates a new user."""
password = serializers.CharField(max_length=128, min_length=8, write_only=True)
class Meta:
model = User
fields = [
'email',
'username',
'password',
'full_name',
]
def validate_email(self, value: str) -> str:
"""Normalize and validate email address."""
valid, error_text = email_is_valid(value)
if not valid:
raise serializers.ValidationError(error_text)
try:
email_name, domain_part = value.strip().rsplit('@', 1)
except ValueError:
pass
else:
value = '@'.join([email_name, domain_part.lower()])
return value
def create(self, validated_data): # type: ignore
"""Return user after creation."""
user = User.objects.create_user(
username=validated_data['username'], email=validated_data['email'], password=validated_data['password']
)
user.full_name = validated_data.get('full_name', '')
user.save(update_fields=['full_name'])
return user
class CustomerRegistrationSerializer(serializers.ModelSerializer[User]):
"""Serializers registration requests and creates a new user."""
password = serializers.CharField(max_length=128, min_length=8, write_only=True)
class Meta:
model = User
fields = [
'email',
'username',
'password',
'full_name',
]
def validate_email(self, value: str) -> str:
"""Normalize and validate email address."""
valid, error_text = email_is_valid(value)
if not valid:
raise serializers.ValidationError(error_text)
try:
email_name, domain_part = value.strip().rsplit('@', 1)
except ValueError:
pass
else:
value = '@'.join([email_name, domain_part.lower()])
return value
def create(self, validated_data): # type: ignore
"""Return user after creation."""
customer = User.objects.create_customer(username=validated_data['username'], email=validated_data['email'], password=validated_data['password'])
customer.full_name = validated_data.get('full_name', '')
customer.is_customer = True
customer.save(update_fields=['full_name','is_customer'])
return customer
class TechnicianRegistrationSerializer(serializers.ModelSerializer[User]):
"""Serializers registration requests and creates a new user."""
password = serializers.CharField(max_length=128, min_length=8, write_only=True)
class Meta:
model = User
fields = [
'email',
'username',
'password',
'full_name',
]
def validate_email(self, value: str) -> str:
"""Normalize and validate email address."""
valid, error_text = email_is_valid(value)
if not valid:
raise serializers.ValidationError(error_text)
try:
email_name, domain_part = value.strip().rsplit('@', 1)
except ValueError:
pass
else:
value = '@'.join([email_name, domain_part.lower()])
return value
def create(self, validated_data): # type: ignore
"""Return user after creation."""
technician = User.objects.create_technician(username=validated_data['username'], email=validated_data['email'], password=validated_data['password'])
technician.full_name = validated_data.get('full_name', '')
technician.is_technician = True
technician.save(update_fields=['full_name','is_technician'])
print(technician.tokens)
return technician
我尝试过调整创建功能或为客户和技术人员自定义用户名或电子邮件,但这些都不适合我。
目前为止你实现的几乎就是Django的多表继承实现。您需要将用户名移动到扩展类(并且实际上在代码中扩展它,而不仅仅是概念上)。
您可以在文档中了解如何实现这一点。
你的代码应该看起来像这样:
class Customer(User):
username = models.CharField(db_index=True, max_length=255, unique=True)
objects = UserManager()
class Technician(User):
username = models.CharField(db_index=True, max_length=255, unique=True)
在底层,django 在这些模型上实现了一对一的字段,这提供了一些简洁的语法简化,但公平警告这可能会给进一步的实现带来一些挑战。
至于什么是最好的解决方案,没有最好的解决方案,只有最适合您需求的解决方案。最好不要想得太远,而是面对挑战。谁知道您最终可能会更喜欢两种桌子系统。另一方面,共享某些属性的用户超类可能会很有用。
更多选项请查看this