Search code examples
djangorestdjango-rest-frameworkdjango-serializerdjango-rest-auth

Django serializer with a filed that has a OneToOne relationship


In my project I have two 'types' of users: Customers and Businesses. They are an extension of the django base User from django.contrib.auth.models.User.

I have in my models.py:

class Customer(models.Model):
    user = models.OneToOneField(User, related_name='user', on_delete=models.CASCADE)

    birth_date = models.DateField(blank=True, null=True)
    phone = PhoneNumberField(unique=True)

    def __str__(self):
        return self.user.username

class Business(models.Model):
    user = models.OneToOneField(User, related_name='business', on_delete=models.CASCADE)

    cf = models.CharField(max_length=16, validators=[ssn_validation])
    birth_date = models.DateField(null=True)
    city = models.CharField(max_length=50, blank=False)
    address = models.CharField(max_length=150, blank=False)

Ok, then I have two different registration, one for Customers and one for Businesses. A problem is that, to validate the password, sent from a REST API, I need to compare password with password2, create a User (django base), and pass it to my Customer.objects.create, like:

I have in my serializers.py:

class CustomerRegistationSerializer(serializers.ModelSerializer):
    username = serializers.CharField(source='user.username',
                                     validators=[UniqueValidator(queryset=User.objects.all())])
    email = serializers.CharField(source='user.email',
                                  validators=[UniqueValidator(queryset=User.objects.all())])
    first_name = serializers.CharField(source='user.first_name')
    last_name = serializers.CharField(source='user.last_name')
    password = serializers.CharField(source='user.password', write_only=True)
    password2 = serializers.CharField(style={'input_style': 'password'}, write_only=True)

    birth_date = serializers.CharField(required=False)

    class Meta:
        model = Customer
        fields = ['id', 'username', 'email', 'password', 'password2', 'first_name', 'last_name',
                  'birth_date', 'phone']

    def save(self):
        username = self.validated_data['user']['username']
        password = self.validated_data['user']['password']
        password2 = self.validated_data['password2']
        email = self.validated_data['user']['email']
        first_name = self.validated_data['user']['first_name']
        last_name = self.validated_data['user']['last_name']
        phone = self.validated_data['phone']

        try:
            birth_date = self.validated_data['birth_date']
        except KeyError:
            birth_date = None

         if password != password2:
            raise serializers.ValidationError({'password': 'Passwords must match!'})

        user = User.objects.create(username=username, email=email, first_name=first_name, last_name=last_name)
        user.set_password(password)
        user.is_active = False
        user.save()

        customer = Customer.objects.create(user=user,
                                           birth_date=birth_date,
                                           phone=phone)
        return customer

That's actually working, but in case of errors can happen that a User is created, but a Customer not. Is there a cleaner way to make Customers registration, always checking for password == password2?

EDIT: I found a more elegant way to handle this:

@transaction.atomic def save(self): password = self.validated_data['user']['password'] password2 = self.validated_data['password2']

user = User.objects.create(**self.validated_data['user'])

if password != password2:
    raise serializers.ValidationError({'password': 'Passwords must match!'})
user.set_password(password)
user.is_active = False
user.save()
update_last_login(None, user)

del self.validated_data['user']
del self.validated_data['password2']

customer = Customer.objects.create(user=user, **self.validated_data)
return customer

Solution

  • If you want to require that all of the DB transactions you are making during save() method are successful to effectively write it on DB, and not to write anything if there is an error at any point in the process, you are typically asking for atomicity (one of the four ACID capabilities of a Database)

    Use this Django decorator, typically made for this:

    from django.db import transaction
    
        @transaction.atomic    
        def save(self):
            <...>