django rest framework - Save extended user data to db

Viewed 411

I try implement registration endpoint for user with additional attributes like phone_number and full_name.

I implement the logic for saving user data, which come from request, but I can't understand how I can save profile data, like phone_number and full_name.

I have a user model:

class User(AbstractUser):
    email = models.EmailField(_('email address'), unique=True)
    is_verified = models.BooleanField(_('is verified by admin'), default=False)

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

    objects = UserManager()

    def __str__(self):
        return self.email

    def save(self, *args, **kwargs):
        if not self.username:
            self.set_username()
        super().save(*args, **kwargs)

    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)

    def set_username(self):
        pass

And profile model:

class Profile(TimeStampedModel):
    STATUS = Choices(
        ("inactive", _("inactive")),
        ("active", _("active")),
        ("banned", _("banned"))
    )

    user = models.OneToOneField(
        User,
        verbose_name=_('related user'),
        on_delete=models.CASCADE,
        related_name='profile',
        related_query_name='profile'
    )
    description = models.TextField(
        _("description of user's profile"),
        blank=True,
        default=''
    )
    status = StatusField(
        _("status of the user")
    )
    birth_date = models.DateField(
        _("Date of birth"),
        validators=[
            MinValueValidator(
                datetime.date(1910, 1, 1)
            ),
            MaxValueValidator(
                datetime.date.today
            )
        ],
        null=True,
        blank=True
    )
    avatar = models.OneToOneField(
        "files.Avatar",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="profile"
    )

    full_name = models.CharField(
        max_length=30,
        default='',
        blank=False
    )
    phone_number_regex_validator = RegexValidator(regex=COMPILED_REGEXP)

    phone_number = models.CharField(
        max_length=16,
        default='',
        blank=False,
        validators=[phone_number_regex_validator]
    )
    objects = ProfileQuerySet.as_manager()

    def __str__(self):
        return f"profile of user {self.user_id}"

    class Meta:
        verbose_name = _('profile')
        verbose_name_plural = _('profiles')

Here is my serealizer for users:

class RegisterSerializer(serializers.Serializer):  # pylint: disable=abstract-method

    email = serializers.EmailField(
        required=True, help_text=_('Email address')
    )
    password1 = serializers.CharField(
        write_only=True, help_text=_('Password')
    )
    password2 = serializers.CharField(
        write_only=True, help_text=_('Password Confirmation')
    )
    phone_number = serializers.CharField(
        write_only=True, help_text=_('Phone number')
    )
    full_name = serializers.CharField(
        write_only=True, help_text=_('Full name')
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cleaned_data = {}

    @staticmethod
    def validate_email(email):
        email = get_adapter().clean_email(email)
        if email and email_address_exists(email):
            raise serializers.ValidationError(_(
                "A user is already registered with this e-mail address.")
            )
        return email

    @staticmethod
    def validate_password1(password):
        return get_adapter().clean_password(password)

    def validate(self, attrs):
        if attrs['password1'] != attrs['password2']:
            raise serializers.ValidationError(_(
                "The two password fields didn't match.")
            )
        return attrs

    def custom_signup(self, request, user):
        pass

    def get_cleaned_data(self):
        return {
            'password1': self.validated_data.get('password1', ''),
            'email': self.validated_data.get('email', ''),
            'phone_number': self.validated_data.get('phone_number', ''),
            'full_name': self.validated_data.get('full_name', '')

        }

    def save(self, request):  # pylint: disable=arguments-differ
        adapter = get_adapter()
        user = adapter.new_user(request)
        self.cleaned_data = self.get_cleaned_data()
        adapter.save_user(request, user, self)
        self.custom_signup(request, user)
        setup_user_email(request, user, [])
        return user

And signals where profile object created and save to db:

@receiver(post_save, sender=User)
def create_user_profile(sender, instance=None, created=False, **kwargs):
    if created:
        Profile.objects.create(user=instance)

How i can save all neded attrs for model profile in the db?

2 Answers

I would just set up the customer profile in the save method, rather than using a signal here. In here you can add the @transaction.atomic decorator to the view, so it ensure either both rows are created or neither rows are created.

If you really want to use the signal, you would need to write your own one that sends the entire validated_data over.

See: https://docs.djangoproject.com/en/3.2/topics/signals/#defining-and-sending-signals

On a side note,

  1. The get_cleaned_data method looks a bit weird to me since all fields in the Serializers are required, you can just use self.validated_data in the save method.

  2. your validate_<field> method should just be instance methods rather than class methods, according to the DRF doc, you can ignore your linter here.

See https://www.django-rest-framework.org/api-guide/serializers/#field-level-validation

This is something related which I did, this is not the exact answer but closely related. I was adding a USER-GROUP, BRANCH to a user at the time of registration. Check the SUPER keyword in Views.py.

I extended the user model so that I can get all information about the user in one place.

This is my model.py

from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
    gender = models.CharField(max_length=10,blank = True)
    first_name = models.CharField(max_length=30,blank = True)
    last_name = models.CharField(max_length=30,blank = True)
    nickname = models.CharField(max_length=30, blank = True)
    dob = models.DateField(null = True,blank = True)
    mobileNumber =  models.CharField(max_length=15,blank = True)
    maritalStatus = models.CharField(max_length=10,blank = True)
    localAddLine1 = models.CharField(max_length=150, blank=True)
    branch = models.ForeignKey(branch,on_delete=models.CASCADE, related_name = 
        'user_brn_data', blank = True, null=True)
    user_group = models.ForeignKey(user_group,on_delete=models.CASCADE, 
        related_name = 'user_group_data', blank = True, null=True)

This is my serializers.py

from django.contrib.auth import get_user_model
User = get_user_model()
class UserSerializers(serializers.ModelSerializer):
    class Meta:
        model = User
#        fields = ("__all__")
        exclude = ['password']

And this is my View.js

from rest_auth.registration.views import RegisterView
from django.contrib.auth import get_user_model
User = get_user_model()
class CuustomRegisterView(RegisterView):
    def create(self, request, *args, **kwargs):
        response = super().create(request, *args, **kwargs)
        data = request.data
        branch = data['branchId']
        groups = data['userGroupId']
        email = data['email']
        isSuperUser = data['isSuperUser']
        User.objects.filter(email=email).update(branch=branch, 
        is_superuser=isSuperUser, user_group=groups, branch =branch)
        return response

You can use the SUPER keyword of python to call the main function and extend the role of it to achieve what you are looking for.

Related