Django - Allow duplicate user's emails in the grand User model, but unique in its extended models?
I'm working on a project in Django which have a User model extend from AbstractBaseUser; and 2 models extend from User (Customer and Technician). Its working all fine when creating a user or customer or technician.
However, whenever I register a new user as either customer or technician (/api/customer/register/ or /api/customer/register/) with the same email (because I define email as username USERNAME_FIELD), it returned me with "This email is already exists." Sure, I understand this since I define the UNIQUE constraint on username and email field in the User model. I want customer to be unique in its own model but not in User model, same for technician. Just like when you sign up for Uber or DoorDash, either you want to be a customer or you want to be a driver or you could be both. AND you could use the same email address to sign up for both. BUT, you cannot use the same email address to sign up again, if you are already signed up with that email as a customer or a driver
I have tried to tweak around this but none works so far! My question is, what would be the best approach to this problem? Should I create 2 different tables for Customer and Technician? If anyone knows, what architecture Uber or DoorDash or Lyft use to approach this problem?
Thank you so much for any help! I would post my models.py here for you fellows to see. Cheers!
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)
My serializers.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
I have tried to tweak around with the create function or customize username or email for Customer and Technician, but none of that works for me.