Search code examples
djangodjango-rest-frameworkdjango-rest-authdjango-rest-viewsets

Django Rest Framework custom serializer's ValidationError not working


I am trying to set up a custom login serializer in Django and want a custom response but the default one always show:

{
    "username":[
        "This field is required."
    ],
    "password":[
        "This field is required."
    ]
}

I tried to set up my serializer like so:

 class MyLoginSerializer(serializers.Serializer):
    username = serializers.CharField(required=True, allow_blank=True)
    email = serializers.EmailField(required=False, allow_blank=True)
    password = serializers.CharField(style={'input_type': 'password'})
    def authenticate(self, **kwargs):
        return authenticate(self.context['request'], **kwargs)
    def _validate_email(self, email, password):
        user = None
        if email and password:
            user = self.authenticate(email=email, password=password)
        else:
            msg = _('Must include "email" and "password".')
            raise serializers.ValidationError(msg)
        return user
    def _validate_username(self, username, password):
        print("in username")
        user = None
        if username and password:
            print("in username 2")
            try:
                user = self.authenticate(username=username, password=password)
            except Exception:
                raise serializers.ValidationError("Wrong")
        else:
            print("in username 3")
            msg = _('Must include "username" and "password".')
            raise serializers.ValidationError(msg)
        return user
    def _validate_username_email(self, username, email, password):
        user = None
        if email and password:
            user = self.authenticate(email=email, password=password)
        elif username and password:
            user = self.authenticate(username=username, password=password)
        else:
            msg = _(
                'Must include either "username" or "email" and "password".'
                )
            raise serializers.ValidationError(msg)
        return user
    def validate(self, attrs):
        username = attrs.get('username')
        email = attrs.get('email')
        password = attrs.get('password')
        user = None
        if 'allauth' in settings.INSTALLED_APPS:
            from allauth.account import app_settings
            # Authentication through email
            if (app_settings.AUTHENTICATION_METHOD ==
                    app_settings.AuthenticationMethod.EMAIL):
                user = self._validate_email(email, password)
            # Authentication through username
            elif (app_settings.AUTHENTICATION_METHOD ==
                    app_settings.AuthenticationMethod.USERNAME):
                user = self._validate_username(username, password)
            # Authentication through either username or email
            else:
                user = self._validate_username_email(username, email, password)
        else:
            # Authentication without using allauth
            if email:
                try:
                    username = GameUser.objects\
                        .get(email__iexact=email)\
                        .get_username()
                except UserModel.DoesNotExist:
                    pass
            if username:
                user = self._validate_username_email(username, '', password)
        # Did we get back an active user?
        if user:
            if not user.is_active:
                msg = ('User account is disabled.')
                raise exceptions.ValidationError(msg)
        else:
            msg = ('Wrong login information.')
            raise exceptions.ValidationError(msg)
        # If required, is the email verified?
        if 'rest_auth.registration' in settings.INSTALLED_APPS:
            from allauth.account import app_settings
            if app_settings.EMAIL_VERIFICATION == app_settings\
                    .EmailVerificationMethod\
                    .MANDATORY:
                email_address = user.emailaddress_set.get(email=user.email)
                if not email_address.verified:
                    raise serializers.ValidationError((
                        'E-mail is not verified.'
                    ))
        attrs['user'] = user
        return attrs

And I have this set as my login serializer in my settings.py:

REST_AUTH_SERIALIZERS ={
    'LOGIN_SERIALIZER': 'api.serializer.MyLoginSerializer'
}

And here is my custom login view:

class CustomLoginView(LoginView):
    permission_classes = (AllowAny,)
    serializer_class = MyLoginSerializer
    def get_response(self):
        original_response = super().get_response()
        print("ORIGINAL REESPONSE:")
        print(str(self.user))
        mydata = {"username": str(self.user), "status": "success"}
        original_response.data.update(mydata)
        return original_response

How would I get it to show the custom 'Must include "email" and "password".' instead of the default message?

Thanks!


Solution

  • Short answer, use the serializer to validate the data then customize the responses based off of the serializers validator. You could even go so far as to use cases.

    The basic structure for this is implemented in your view/viewset like:

    class MyViewSet(viewsets.ModelViewSet):
        """
        My View Set
        """
        queryset = #
        serializer_class = #
    
        # take which ever method you want to add the custom formatting to
        # and modify it, or create a new method with its own special endpoint
        def view_set_method_to_modify(self, *other_args):
    
            # Do some stuff to keep the desired functions of the method you are hacking
    
            serializer = serializers.MySerializer(data=request.data, context={"request": request})
            if serializer.is_valid():
    
                # Do some stuff with serializer.validated_data['my_var'])
    
                return Response({'message': 'Yay the data is valid!!!'},
                    status=status.HTTP_200_OK)
            return Response(serializer.errors,
                status=status.HTTP_400_BAD_REQUEST)
    
    

    Here is an example that I have working to update user passwords. Instead of hacking an existing method, I ended up adding a new method with its own special endpoint.

    views.py

    class UserViewSet(viewsets.ModelViewSet):
        """
        User View Set
        """
        queryset = User.objects.all()
        serializer_class = serializers.UserSerializer
    
        @action(methods=['post'], detail=True,
                url_path='change-password', url_name='change_password')
        def set_password(self, request, pk=None):
            user = self.get_object()
            serializer = serializers.PasswordSerializer(data=request.data, context={"request": request})
            if serializer.is_valid():
                user.set_password(serializer.validated_data['new_password'])
                user.save()
                return Response({'message': 'password set'},
                    status=status.HTTP_200_OK)
            return Response(serializer.errors,
                status=status.HTTP_400_BAD_REQUEST)
    

    serializers.py

    class PasswordSerializer(Serializer):
        old_password = CharField(required=True)
        new_password = CharField(required=True)
    
        def validate(self, data):
            request = self.context['request']
            user = request.user
            new_password = request.data['new_password']
            old_password = request.data['old_password']
            validate_passwords(old=old_password, new=new_password, user=user)
            return data