Отдельная модель электронной почты от пользовательской модели AbstractBaseUser

#python #django #django-models

#python #django #django-модели

Вопрос:

Мне было интересно преобразовать поле электронной почты из AbstractBaseUser в моей пользовательской реализации в собственную модель. Идея в том, что я могу потребовать от пользователей зарегистрироваться, указав адрес электронной почты, а затем, как только они подтвердят, я смогу продолжить и записать создание записи пользователя в таблицу User.

Однако я сталкиваюсь с ошибкой с django/db/models/fields/related_descriptors.py когда я пытаюсь создать суперпользователя (я уже настроил UserManager так, чтобы не normalize_email для моего внешнего ключа электронной почты). Ошибка гласит:

 ValueError: Cannot assign "1": "User.email" must be a "Email" instance
  

Я не уверен, как конкретно исправить эту ошибку, и буду признателен за любую помощь. Ниже приведена моя реализация:

* Кроме того, является ли разбиение электронной почты на собственную таблицу излишне сложным?

Models.py

 from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.auth.models import PermissionsMixin #, UserManager
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.contrib.auth.base_user import AbstractBaseUser
from django.core.validators import EmailValidator, MinLengthValidator
from django.core.mail import send_mail
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from .utils import CustomUsernameValidator, username_blacklist_validator
from .managers import EmailManager, UserManager

from phonenumber_field.modelfields import PhoneNumberField
from enum import Enum


def avatar_upload_to(instance, filepath):
    return 'user_images/{username}/avatar/{filepath}'.format(username=instance.user.id, filepath=filepath)

class GenderChoice(Enum):
    """Subclass of Enum for gender profile choices"""
    MALE = "Male"
    FEMALE = "Female"
    F2M = "F2M"
    M2F = "M2F"
    OTHER = "Other"

class Gender(models.Model):
    """
    Gender dimension table
    """
    #gender = models.CharField(
    #   _('gender'),
    #   max_length=32,
    #   choices=[(tag.name, tag.value) for tag in GenderChoice],     
    #       default=GenderChoice.UNSPECIFIED.value
    #       null=True,
    #       blank=True
    #    )
    gender = models.CharField(_('gender'), max_length=32, null=True, blank=True)


class Email(models.Model):
    """
    User email address information
    """

    # Validators
    email_validator = EmailValidator()

    # Model fields
    email = models.EmailField(
        _('email address'),
        unique=True,
        validators=[email_validator]
    )
    signup_date = models.DateTimeField(_('date joined'),     default=timezone.now)
    is_verified = models.BooleanField(
        _('verified'),
        default=False,
        help_text=_(
            'Designates whether the email address is active in the system     via a verification link.'
        ),
    )
    verification_sent_datetime = models.DateTimeField(
        _('verification sent date'),
        default=timezone.now
    )
    verification_expiration_datetime = models.DateTimeField(
        _('verification expiration date'),
        default=timezone.now()   timezone.timedelta(hours=2)
    )
    verification_sent_count = models.IntegerField(
        _('verification sent count'),
        default=1
    )

    objects = EmailManager()

    EMAIL_FIELD = 'email'
    REQUIRED_FIELDS = ['email']

    class Meta:
        ordering = ['email']
        verbose_name = _('email')
        verbose_name_plural = _('emails')

    def __str__(self):
        """
        Returns a string representation of this 'User'. This string is used     when a 'User' is printed in the console.
        """
        return self.email

    def email_user(self, subject, message, from_email=None, **kwargs):
        """
        Send an email to this user.
        """
        send_mail(subject, message, from_email, [self.email], **kwargs)


class PhoneNumber(models.Model):
    """
    User phone number information
    """
    phone_number = PhoneNumberField(null=True, blank=False)     #default=' 10000000000')
    associated_account_count = models.IntegerField(_('associated account     count'), default=0)


class User(AbstractBaseUser, PermissionsMixin):
    """
    Custom user to be used for the Titan project
    """

    # Validators
    username_validator = UnicodeUsernameValidator()
    custom_username_validator = CustomUsernameValidator()

    # Default values
    avatar_default = 'user_images/_default/avatar/default.jpg'

    # Model fields
    email = models.OneToOneField(
        Email, on_delete=models.CASCADE, primary_key=False
    )
    phone_number = models.ForeignKey(PhoneNumber, on_delete=models.SET_NULL,     blank=False, null=True, primary_key=False)
    username = models.CharField(
        _('username'),
        max_length=20,
        unique=True,
        help_text=_('Letters, numbers, dashes, and underscores only.     Username must be between 3 and 20 characters.'),
        validators=[
            username_validator, 
            custom_username_validator,
            username_blacklist_validator,
            MinLengthValidator(3),
        ],
        error_messages={
            'unique': _('A user with that username already exists.'),
        },
    )
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin     site.')
        )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        )
    )
    date_joined = models.DateTimeField(_('date joined'),     default=timezone.now)
    name = models.CharField(_('name'), max_length=100, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    city = models.CharField(max_length=128, null=True, blank=True)
    country = models.CharField(max_length=128, null=True, blank=True)
    gender = models.ForeignKey(Gender, on_delete=models.SET_NULL, null=True)
    bio = models.TextField(max_length=500, null=True, blank=True)
    avatar = models.ImageField(max_length=255, null=False, blank=False,     default=avatar_default) #avatar = models.ImageField(upload_to=upload_to,     max_length=255, default=avatar_default)
    website = models.CharField(max_length=255, null=True, blank=True)
    is_private = models.BooleanField(_('private'), default=False, help_text=_(
        'Designates whether this user profile is private.'
        )
    )
    friend_count = models.IntegerField(_('friend count'), null=False,     blank=False, default=0)
    follower_count = models.IntegerField(_('follower cou nt'), null=False,     blank=False, default=0)
    is_verified = models.BooleanField(_('verified'), null=False, blank=False, default=False, help_text=_(
        'Designates whether the user has gone through government issued id     verification.'
        )
    )

    objects = UserManager()

    EMAIL_FIELD = 'email__email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        ordering = ['username']
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def __str__(self):
        """
        Returns a string representation of this 'User'. This string is used     when a 'User' is printed in the console.
        """
        return self.username

    def get_name(self):
        """
        Returns the name of the user.
        """
        return name.strip()

    def email_user(self, subject, message, from_email=None, **kwargs):
        """
        Send an email to this user.
        """
        send_mail(subject, message, from_email, [self.email_id__email],     **kwargs)

    # The following are class bits based off of the nnmware NnmwareUser     model
    def _ctype(self):
        return ContentType.objects.get_for_model(get_user_model())


class PhoneNumberVerification(models.Model):
    """
    Phone number verification table. We allow phone numbers to be associated     with multiple
    accounts unlike email addresses so use a separate table to track verification.
    """
    phone_number = models.ForeignKey(PhoneNumber, on_delete=models.CASCADE, null=False, blank=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=False,     blank=False)
    signup_date = models.DateTimeField(_('date joined'),     default=timezone.now)
    is_verified = models.BooleanField(
        _('verified'),
        default=False,
        help_text=_(
            'Designates whether the email address is active in the system     via a verification link.'
        ),
    )
    verification_sent_datetime = models.DateTimeField(_('verification sent     date'),
        default=timezone.now
    )
    verification_expiration_datetime = models.DateTimeField(
        _('verification expiration date'),
        default=timezone.now()   timezone.timedelta(hours=2)
    )
    verification_sent_count = models.IntegerField(
        _('verification sent count'),
        default=1
    )
  

Managers.py

 from django.contrib.auth.base_user import BaseUserManager

class EmailManager(BaseUserManager):

    def _create_email(self, email, **extra_fields):
        """
        Create and save a user email address.
        """
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        #extra_fields.setdefault('is_verified', False)
        #extra_fields.setdefault('verification_expiration_datetime',     timezone.now   timedelta(hours=2)
        useremail.save(using=self._db)
        return useremail


class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, username, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        if not username:
            raise ValueError('The given username must be set')
        if not self.verify_is_integer(email):
            raise ValueError('The given email is not an integer')
        email = email #email = self.normalize_email(email)
        username = self.model.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(username, email, password, **extra_fields)

    def verify_is_integer(self, field):
        return isinstance(field, int)
  

Ответ №1:

Я думаю, вы можете обновить свой _create_user метод следующим образом:

 def _create_user(self, username, email, password, **extra_fields):
    """
    Create and save a user with the given username, email, and password.
    """
    if not username:
        raise ValueError('The given username must be set')
    if not self.verify_is_integer(email):
        raise ValueError('The given email is not an integer')
    username = self.model.normalize_username(username)
    user = self.model(username=username, email_id=email, **extra_fields). # using email_id instead of email
    user.set_password(password)
    user.save(using=self._db)
    return user
  

Здесь email поле ожидает экземпляр электронной почты, но здесь вы создаете экземпляр модели электронной почты и передаете его первичный ключ в качестве параметра. Поэтому использование email_id вместо email должно решить вашу проблему.

Комментарии:

1. Блестяще — большое вам спасибо! Я полностью пропустил эту строку в методе _create_user.