Search code examples
djangodjango-rest-frameworkdjango-rest-auth

How to implement a custom detail serializer on django-rest-auth and serialize it


I want to create a profile user when an instance of user is created, i get it but when i run the server it gives me TypeError saying that "NoneType" object is not iterable, i created a post_save signal in UserProfile model:

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

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

my UserProfile model is:

class UserProfile(TimeStampedModel):
MALE = 'M'
FEMALE = 'F'
NOT_SPECIFIED = 'NS'

GENDER_CHOICES = (
    (MALE, _('Male')),
    (FEMALE, _('Female')),
    (NOT_SPECIFIED, _('Not specified'))
)

VALIDATOR = [validators.RegexValidator(re.compile('^[\w]+$'),
                                       _('Only can has letters'), 'invalid')]

user_profile_id = models.AutoField(primary_key=True)
user = models.OneToOneField(auth_user, verbose_name=_('user'), blank=False, null=False,
                            on_delete=models.CASCADE, related_name='profile')
first_name = models.CharField(max_length=125, verbose_name=_('first name'),
                              validators=VALIDATOR, blank=True, null=False)
last_name = models.CharField(max_length=125, verbose_name=_('last name'),
                             validators=VALIDATOR, blank=True, null=False)
location = models.ForeignKey(Locations, on_delete=models.DO_NOTHING, verbose_name=_('location'),
                             related_name='location', blank=True, null=True)
profile_image = models.ImageField(verbose_name=_('profile image'), null=True)
gender = models.CharField(verbose_name=_('gender'), max_length=2, choices=GENDER_CHOICES, blank=True, null=True,
                          default=NOT_SPECIFIED)
DOB = models.DateField(verbose_name=_('date of birth'), blank=True, null=True)
occupation = models.TextField(verbose_name=_('occupation'), blank=True, null=False)
about = models.TextField(verbose_name=_('about'), blank=True, null=False)

I'm using django-rest-framework, django-rest-auth and django-allauth. It's my UserProfileSerializer:

class UserProfileSerializer(UserDetailsSerializer):
# profile_image = ImageSerializer()
user = UserSerializer(source='profile', many=True)

class Meta:
    model = UserProfile
    fields = '__all__'
    read_only_fields = ('created_at', 'updated_at',)

def update(self, instance, validated_data):
    instance.first_name = validated_data.get('first_name', instance.first_name)
    instance.last_name = validated_data.get('last_name', instance.last_name)
    instance.occupation = validated_data.get('occupation', instance.occupation)
    instance.about = validated_data.get('about', instance.about)
    instance.save()
    return instance

def to_internal_value(self, data):
    user_data = data['user']
    return super().to_internal_value(user_data)

def to_representation(self, instance):
    pass

When i access 127.0.0.1:800/rest-auth/user or 127.0.0.1:800/rest-auth/registration and register a user appear the following output:

TypeError at /rest-auth/user/
'NoneType' object is not iterable
Request Method: GET
Request URL:    http://127.0.0.1:8000/rest-auth/user/
Django Version: 3.0.1
Exception Type: TypeError
Exception Value:    
'NoneType' object is not iterable
Exception Location: C:\PROGRA~1\Python37\venv\lib\site-packages\rest_framework\utils\serializer_helpers.py in __init__, line 18
Python Executable:  C:\PROGRA~1\Python37\venv\Scripts\python.exe
Python Version: 3.7.2
Python Path:    
['C:\\Program Files\\Python37\\venv\\Scripts\\asta',
 'C:\\PROGRA~1\\Python37\\venv\\Scripts\\python37.zip',
 'C:\\PROGRA~1\\Python37\\venv\\DLLs',
 'C:\\PROGRA~1\\Python37\\venv\\lib',
 'C:\\PROGRA~1\\Python37\\venv\\Scripts',
 'c:\\program files\\python37\\Lib',
 'c:\\program files\\python37\\DLLs',
 'C:\\PROGRA~1\\Python37\\venv',
 'C:\\PROGRA~1\\Python37\\venv\\lib\\site-packages',
 'C:\\Program Files\\Python37\\venv\\Scripts\\asta\\conf\\backend']
Server time:    Qui, 6 Fev 2020 15:01:44 +000

My base.py(settings) is:

    REST_FRAMEWORK = {
    # CHANGE IT, to use oauth2 by django-oauth2-toolkit : already
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        # 'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}

REST_AUTH_REGISTER_SERIALIZERS = {
    'REGISTER_SERIALIZER': 'backend.users.serializers.SignUpSerializer'
}

REST_AUTH_SERIALIZERS = {
    'USER_DETAILS_SERIALIZER': 'backend.users.serializers.UserProfileSerializer',
    'LOGIN_SERIALIZER': 'backend.users.serializers.CustomLoginSerializer'
}

REST_USE_JWT = True
ACCOUNT_LOGOUT_ON_GET = True
OLD_PASSWORD_FIELD_ENABLED = True
SOCIALACCOUNT_QUERY_EMAIL = True
CORS_ORIGIN_ALLOW_ALL = True

JWT_AUTH = {
    'JWT_VERIFY_EXPIRATION': False
}

When i change user = UserSerializer(source='profile', many=True) to user = UserSerializer(source='profile') I got other error :

AttributeError at /rest-auth/user/
Got AttributeError when attempting to get a value for field `username` on serializer `UserSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `UserProfile` instance.
Original exception text was: 'UserProfile' object has no attribute 'username'.

Solution

  • You should remove the many=True flag on the UserProfileSerializer

    class UserProfileSerializer(UserDetailsSerializer):
    # profile_image = ImageSerializer()
    user = UserSerializer(source='profile')
    

    According to the documentation:

    If the field is used to represent a to-many relationship, you should add the many=True flag to the serializer field.

    So a to-one relationship shouldn't have the many=True flag.

    Edit

    Ok so, after the edit another problem became clear. In your settings you change 'USER_DETAILS_SERIALIZER' in 'backend.users.serializers.UserProfileSerializer'. If you read the documentation of django-rest-auth about how the /rest-auth/user/ url is implemented you can see that they use UserDetailsView and pass an instance of User to the serializer. Thus either you make your custom UserDetailsView or, if you want to keep the implementation of the library, you need to extend the implementation of the library and handle the One-to-One Relationship there.

    Note 1: In Django Rest Framework nested serializers aren't writeable by default. So you'll need to implement the update logic yourself or you can seperate the UserProfile update logic from the UserDetailsView in your own view.

    Note 2: You can edit the RegisterSerializer of Django Rest Auth and make sure the UserProfile is created there so you can handle the 'nested creation' in that serializer instead of using a signal by getting the data in one API call.

    Edit 2

    As per request an example, quite opiniated trying to follow the package.

    REST_AUTH_SERIALIZERS = {
        # Changed
        'USER_DETAILS_SERIALIZER': 'backend.users.serializers.CustomUserDetailsSerializer',
    
        # Not changed
        'LOGIN_SERIALIZER': 'backend.users.serializers.CustomLoginSerializer'
    }
    

    Make your CustomUserDetailsSerializer

    # Get the UserModel
    UserModel = get_user_model()
    
    # Your UserProfileSerializer
    class UserProfileSerializer(serializers.ModelSerializer):
        # profile_image = ImageSerializer()
    
        class Meta:
            model = UserProfile
            fields = '__all__'
            read_only_fields = ('created_at', 'updated_at',)
    
    # Custom serializer based on rest_auth.serializers.UserDetailsSerializer
    class CustomUserDetailsSerializer(serializers.ModelSerializer):
        """
        User model w/o password
        """
        user_profile = UserProfileSerializer()
    
        class Meta:
            model = UserModel
            fields = ('pk', 'username', 'email', 'user_profile')
            read_only_fields = ('email', )
    
        # Nested writes are not possible by default so you should implement it yourself
        # https://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers
    
        def update(self, instance, validated_data):
            # Do updates on instance and save
            # updates...
            # eg. instance.username = validated_data.get('username', instance.username)
    
            # get user profile data
            user_profile_data = validated_data.pop('user_profile')
    
            # access UserProfile through User instance one-to-one
            user_profile = instance.profile
    
            # do updates on user_profile with data in user_profile_data
            # updates...
    
            # save
            user_profile.save()
    
            return instance
    

    Example JSON

    {
       "pk": 1,
       "email": "test@test.com",
       "username":"test",
       "user_profile":{
          "first_name":"Jane",
          ...
       }
    }