Search code examples
djangoserializationdjango-rest-frameworkrelated-content

HyperlinkedModelSerializer custom lookup_field to related_model


i have the below config and i would like to map the url field in UserProfileView to the related user's username instead of the default pk field

currently the url looks likes below, appreciate any help

{
        "user": 23,
        "bio": "My bio",
        "created_on": "2020-06-12T21:24:52.746329Z",
        "url": "http://localhost:8000/bookshop/bio/8/?format=api"
    },

what i am looking for is

{
        "user": 23,   <--- this is the user <pk> 
        "bio": "My bio",
        "created_on": "2020-06-12T21:24:52.746329Z",
        "url": "http://localhost:8000/bookshop/bio/username/?format=api"
    },
models.py

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    bio = models.CharField(max_length=255)
    created_on = models.DateTimeField(auto_now_add=True)
    last_updated = models.DateTimeField(auto_now=True)

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

class UserProfileViewSets(viewsets.ModelViewSet):

    authentication_classes = [TokenAuthentication, ]
    permission_classes = [rest_permissions.IsAuthenticated, permissions.UserProfileOwnerUpdate, ]
    queryset = models.UserProfile.objects.all()
    serializer_class = serializers.UserProfileSerializer
    renderer_classes = [renderers.AdminRenderer, renderers.JSONRenderer, renderers.BrowsableAPIRenderer, ]
    # lookup_field = 'user.username'

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)
serializer.py

class UserProfileSerializer(serializers.ModelSerializer):

    class Meta:
        model = models.UserProfile
        fields = ['user', 'bio', 'created_on', 'url']
        extra_kwargs = {
            'last_updated': {
                'read_only': True
            },
            'user': {
                'read_only': True
            },
        }

Solution

  • after struggling and reading many articles, I did it and posting down the solution if anybody was looking for the same use case.

    • the fields are being related to each other by OneToOne relationship
    models.py
    
    class UserProfile(models.Model):
        user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
        bio = models.CharField(max_length=255)
        created_on = models.DateTimeField(auto_now_add=True)
        last_updated = models.DateTimeField(auto_now=True)
    
        def __str__(self):
            return self.user.username
    
    class User(AbstractBaseUser, PermissionsMixin):
        """"
        Customizes the default user account
        """
        email = models.EmailField(unique=True, help_text='username is the email address')
        first_name = models.CharField(max_length=40, blank=False)
        last_name = models.CharField(max_length=40, blank=False)
        date_joined = models.DateTimeField(auto_now_add=True)
        is_active = models.BooleanField(default=True)
        is_staff = models.BooleanField(default=False)
        username = models.CharField(max_length=15, unique=True, null=True, blank=False,
                                    validators=(validators.UnicodeUsernameValidator, ))
        is_borrower = models.BooleanField(default=False)
    
    
    • The serializer is a HyperlinkedModelSerializer, as shown below the user SerializerField is PrimaryKeyRelatedField and it is being related to another column/field in the User model user.username - i made this as the default PrimaryKeyRelatedField is the pk and i dont want to expose that on the API

    • the url key is customized to be HyperlinkedRelatedField to point to the above field - the user with a viewname user-related

    serializer.py
    
    class UserProfileSerializer(serializers.HyperlinkedModelSerializer):
    
        user = serializers.PrimaryKeyRelatedField(source='user.username', read_only=True)
        url = serializers.HyperlinkedRelatedField(read_only=True, view_name='user-detail', )
    
        class Meta:
            model = models.UserProfile
            fields = ['user', 'bio', 'created_on', 'url']
            extra_kwargs = {
                'last_updated': {
                    'read_only': True
                },
                'user': {
                    'read_only': True
                },
            }
    
    • on the views, i defined the lookup_field to be user and override the get_object method as now the queryset should be filtered by the username
    views.py
    
    class UserProfileViewSets(viewsets.ModelViewSet):
    
        authentication_classes = [TokenAuthentication, ]
        permission_classes = [rest_permissions.IsAuthenticated, permissions.UserProfileOwnerUpdate, ]
        queryset = models.UserProfile.objects.all()
        serializer_class = serializers.UserProfileSerializer
        renderer_classes = [renderers.AdminRenderer, renderers.JSONRenderer, renderers.BrowsableAPIRenderer, ]
        lookup_field = 'user'
    
        def perform_create(self, serializer):
            serializer.save(user=self.request.user)
    
        def get_object(self):
            queryset = self.filter_queryset(models.UserProfile.objects.get(user__username=self.kwargs.get('user')))
            return queryset
    

    EDIT:

    I did the requirements in another approach and think this one is more neat way , so below the modifications.

    • You need to create anew customized HyperLinkedIdentityField where you over right the kwargs, check the below kwargs, the value is mapped to the related model where a OneToOneForgienKey deifined
    
        class AuthorHyperLinkedIdentityField(serializers.HyperlinkedIdentityField):
            def get_url(self, obj, view_name, request, format):
                if hasattr(obj, 'pk') and obj.pk is None:
                    return None
                return self.reverse(view_name, kwargs={
                    'obj_username': obj.author.username
                }, format=format, request=request)
    
    
    • on the view you overright the lookup_field with the kwargs defined in the CustomizedField
    
        class AuthorViewSet(viewsets.ModelViewSet):
            serializer_class = serializers.AuthorSerializer
            queryset = models.Author.objects.all()
            renderer_classes = [renderers.JSONRenderer, renderers.BrowsableAPIRenderer, renderers.AdminRenderer]
            # the below is not used but i keep it for reference
            # lookup_field = 'author__username'
            # the below should match the kwargs in the customized HyperLinkedIdentityField
            lookup_field = 'obj_username'
    
    
    • The final serializer would look like
    class AuthorSerializer(serializers.HyperlinkedModelSerializer):
        """
        Serializers Author Model
        """
    
        # first_name = serializers.SlugRelatedField(source='author', slug_field='first_name',
        #                                           read_only=True)
        # last_name = serializers.SlugRelatedField(source='author', slug_field='last_name',
        #                                          read_only=True)
        author = serializers.PrimaryKeyRelatedField(queryset=models.User.objects.filter(groups__name='Authors'),
                                                    write_only=True)
        name = serializers.SerializerMethodField()
        username = serializers.PrimaryKeyRelatedField(source='author.username', read_only=True)
        # the below commented line is building the URL field based on the lookup_field = username
        # which takes its value from the username PrimaryKeyRelatedField above
        # url = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True)
        url = AuthorHyperLinkedIdentityField(view_name='author-detail', read_only=True)
    
        class Meta:
            model = models.Author
            fields = ['author', 'name', 'username', 'url', ]
    
        def get_name(self, author):
            return '%s %s' % (author.author.first_name, author.author.last_name)
    
    • below the Author Model for your reference
    class Author(models.Model):
        """
        A Model to store the Authors info
        """
        author = models.OneToOneField(User, on_delete=models.CASCADE, related_name='authors')
        is_author = models.BooleanField(default=True, editable=True, )
    
        class Meta:
            constraints = [
                models.UniqueConstraint(fields=['author'], name='check_unique_author')
            ]
    
        def __str__(self):
            return '%s %s' % (self.author.first_name, self.author.last_name)
    
        def author_full_name(self):
            return '%s %s' % (self.author.first_name, self.author.last_name)