Ahmed Al-Haffar
Ahmed Al-Haffar

Reputation: 552

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
            },
        }

Upvotes: 0

Views: 894

Answers (1)

Ahmed Al-Haffar
Ahmed Al-Haffar

Reputation: 552

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)

Upvotes: 1

Related Questions