#Unable to bypass the field level validation.

40 messages · Page 1 of 1 (latest)

dense crag
#

I've a use-case where I'd to create a custom user model. I'm facing issues when I try to update the email/username fields. Not sure if I'm following the best practices to do the same though, as I'm pretty new to django.

Attached is the code FYI:
Models.py:

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager


class CustomUserManager(UserManager):
    def get_by_natural_key(self, username):
        case_insensitive_username_field = '{}__iexact'.format(self.model.USERNAME_FIELD)
        return self.get(**{case_insensitive_username_field: username})
    

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    username = models.CharField(max_length=50, unique=True)
    name = models.CharField(max_length=100)
    password = models.CharField(max_length=100)
    is_active = models.BooleanField(default=True)

    USERNAME_FIELD = 'username'
    EMAIL_FIELD = 'email'

    objects = CustomUserManager()

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)
        self.username = self.username.lower()

    def save(self, *args, **kwargs):
        if self.password:
            self.set_password(self.password)
        super().save(*args, **kwargs)

Serializers.py:

from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()


class UserRegisterSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['email', 'username', 'name', 'password']
        extra_kwargs = {
            'password':{'write_only':True, 
                        'style':{'input_type':'password'}
                        }
        }

    def update(self, instance, validated_data):
        print("Update logic here")
        return instance
#

Views.py:

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.auth import get_user_model
from django.http import Http404

from .serializers import UserRegisterSerializer

User = get_user_model()


class UserRegisterView(APIView):
    """
    Register a new user
    """
    def post(self, request):
        serializer = UserRegisterSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class UserDetailsView(APIView):

    def _get_user(self, username):
        try:
            return User.objects.get(username=username)
        except User.DoesNotExist:
            raise Http404()
    
    def delete(self, _, username):
        user = self._get_user(username)
        user.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

    def put(self, request, username):
        user = self._get_user(username)
        serializer = UserRegisterSerializer(user, data=request.data, partial=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Even when the partial is set to True, the field level validations still happen and I'm always getting an error:
{
"email": [
"custom user with this email already exists."
]
}

If that's the case then how do I have an API to change the email/username once the user's been created?

thin magnet
#

I think you're having problem here

email = models.EmailField(unique=True)
username = models.CharField(max_length=50, unique=True)

both fields are set to unique, it doesn't matter your logic in your views and serializer. The model doesn't allow it

dense crag
#

ya but if I want to keep both the fields unique, that's the way to go right? @thin magnet

thin magnet
#

If you want you can choose your username to be unique in that case you can set your email not unique so that you can change email for the users. In my opinion it's better to update user's emails rather than changing usernames

dense crag
#

I mean let's say I've a user with email: abc@abc.com and username: abc. In case of me changing the username to def while keeping the email same, I'm getting the validation error that "custom user with this email already exists."

dense crag
thin magnet
#

dont use the serializer for registering new users

lethal glen
#

Also from the discussion on Github for similar problem:

The workaround this is to remove the unique constraint from the serializer and move it to the business logic validation part since the nested serializer can not say - as of today - whether he's updating or creating a new object.

dense crag
lethal glen
#

I think siomething like this:

class UserRegisterSerializer(serializers.ModelSerializer): 
    email = serializers.EmailField(unique=False)
    class Meta:
        model = User
        fields = ['email', 'username', 'name', 'password']
        extra_kwargs = {
            'password':{'write_only':True, 
                        'style':{'input_type':'password'}
                        }
        }
    def update(self, isntance):
        # validate here

dense crag
lethal glen
#

sure you do it will also check for example if it's valid email
'fdfd@email.com'-valid
'fdfdfd%.com'-not valid, but it won't check whether it is unique

#

also you have to call is_valid() otherwise you won't be able to get the data from the serializer if I remember correctly.

dense crag
lethal glen
#

yes right now it checks whether these fields are unique that why you need to override default unique=True from model in the serializer, by 'repeating' this field

    # might differ sligthly
    email = serializers.EmailField(unique=False)

    # maybe this is correct, you have to look it up
    email = serializer.EmailField(validators=[])

in the serializer,

thin magnet
#

No matter logic in the views or serializers, the model won't allow it if it's unique, it lies also in the database

lethal glen
#

but he wants to pass serialization error not model error

thin magnet
#

but serializer checks the database first thats why it's serializing first

dense crag
thin magnet
#

In this case, i would create a different serializer that is able to change user details

#

A function must only do one thing

register = register
update = update

you can't use register to update

lethal glen
#

A different serializer for put/patch is also a valid option but UserRegisterSerializer is simply a case of weird naming, it's just UserSerializer and there is no reason for it to not be used for updating

dense crag
# lethal glen A different serializer for put/patch is also a valid option but `UserRegisterSer...

I've updated the code to something like this.

from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['email', 'username', 'name', 'password']
        extra_kwargs = {
            'password':{'write_only':True, 
                        'style':{'input_type':'password'}
                        }
        }

class UserUpdateSerializer(UserSerializer):
    email = serializers.EmailField(max_length=255)
    username = serializers.CharField(max_length=50)

    def update(self, instance, validated_data):
        """
        Update and validation logic in here.
        """
        print("update in progress")

Now it works as expected!!

lethal glen
thin magnet
dense crag
#

Thank you for your time! @thin magnet @lethal glen

dense crag
lethal glen
#

serialize.is_valid() will validate incoming request before anything in update() runs, however in update() you might validate whether username or email is unique before trying to save it to the databse, drf validators will just give you a nicer response than whatever database throws if it's not unique

#

currently in your case is_valid checks only if it's a valid email and max length of both fields, but before saving to database you should also check if it's a unique username for example

dense crag
# lethal glen currently in your case is_valid checks only if it's a valid email and max length...

yes that I'll do anyhow, but I was planning to do it this way:
https://stackoverflow.com/questions/44794288/django-rest-framework-uniquevalidator-throw-error-when-update-an-item-with-old-d
I'm not sure if I can use the UniqueValidator inside the update method of the serializer.

lethal glen
#

if you mean that you wanted to add:

username = serializers.CharField(
        validators=[UniqueValidator(queryset=User.objects.all())]
    )

then again you might have problems with serializer.is_valid()

#
    def update(self, instance, validated_data):
        """
        Update and validation logic in here.
        """
        print("update in progress")
        uv = UniqueValidator(User.objects.all(), message="Unique failed for username.")
        uv(validated_data.username, username)

something liek this

dense crag
#

No I wanted to do something like this inside the update:

username = validated_data.get('username', '')

if User.objects.exclude(pk=instance.pk).filter(username=username):
    raise serializers.ValidationError('User with this username already exists.')

instance.__dict__.update(**validated_data)
instance.save()

return instance
lethal glen
#

this also looks like a valid option

dense crag
thin magnet