K. Tomov
K. Tomov

Reputation: 140

Change serializers on per-object basis within one ViewSet?

I'm working on a project with some social features and need to make it so that a User can see all details of his profile, but only public parts of others' profiles.
Is there a way to do this within one ViewSet?

Here's a sample of my model:

class Profile(TimestampedModel):
    user = models.OneToOneField(User)

    nickname = models.CharField(max_length=255)
    sex = models.CharField(
        max_length=1, default='M',
        choices=(('M', 'Male'), ('F', 'Female')))

    birthday = models.DateField(blank=True, null=True)

For this model, I'd like the birthday, for example, to stay private.
In the actual model there's about a dozen such fields.

My serializers:

class FullProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = Profile


class BasicProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = read_only_fields = ('nickname', 'sex', 'birthday')

A custom permission I wrote:

class ProfilePermission(permissions.BasePermission):
    """
    Handles permissions for users.  The basic rules are

    - owner and staff may do anything
    - others can only GET
    """

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        else:
            return request.user == obj.user or request.user.is_staff

And my viewset:

class RUViewSet(
        mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
        mixins.ListModelMixin, viewsets.GenericViewSet):
    """ViewSet with update/retrieve powers."""


class ProfileViewSet(RUViewSet):
    model = Profile
    queryset = Profile.objects.all()
    permission_classes = (IsAuthenticated, ProfilePermission)

    def get_serializer_class(self):
        user = self.request.user
        if user.is_staff:
            return FullProfileSerializer
        return BasicProfileSerializer

What I'd like is for request.user's own profile in the queryset to be serialized using FullProfileSerializer, but the rest using BasicProfileSerializer.
Is this at all possible using DRF's API?

Upvotes: 3

Views: 891

Answers (2)

Rahul Gupta
Rahul Gupta

Reputation: 47846

We can override the retrieve() and list methods in our ProfileViewSet to return different serialized data depending on the user being viewed.

In the list method, we serialize all the user instances excluding the current user with the serializer returned from get_serializer_class() method. Then we serialize the current user profile information using the FullProfileSerializer explicitly and add this serialized data to the data returned before.

In the retrieve method, we set a accessed_profile attribute on the view to know about the user the view is displaying. Then, we will use this attribute to decide the serializer in the get_serializer_class() method.

class ProfileViewSet(RUViewSet):
    model = Profile
    queryset = Profile.objects.all()
    permission_classes = (IsAuthenticated, ProfilePermission)

    def list(self, request, *args, **kwargs):
        instance = self.filter_queryset(self.get_queryset()).exclude(user=self.request.user)
        page = self.paginate_queryset(instance)
        if page is not None:
            serializer = self.get_pagination_serializer(page)
        else:
            serializer = self.get_serializer(instance, many=True)
        other_profiles_data = serializer.data # serialized profiles data for users other than current user
        current_user_profile = <get_the_current_user_profile_object>
        current_user_profile_data = FullProfileSerializer(current_user_profile).data
        all_profiles_data = other_profiles_data.append(current_user_profile_data)
        return Response(all_profiles_data)

    def retrieve(self, request, *args, **kwargs):
        self.accessed_profile = self.get_object() # set this as on attribute on the view
        serializer = self.get_serializer(self.accessed_profile)
        return Response(serializer.data)

    def get_serializer_class(self):
        current_user = self.request.user
        if current_user.is_staff or (self.action=='retrieve' and self.accessed_profile.user==current_user):
            return FullProfileSerializer    
        return BasicProfileSerializer

Upvotes: 1

K. Tomov
K. Tomov

Reputation: 140

I managed to hack together the solution that provides the wanted behaviour for the detail view:

class ProfileViewSet(RUViewSet):
    model = Profile
    queryset = Profile.objects.all()
    permission_classes = (IsAuthenticated, ProfilePermission)

    def get_serializer_class(self):
        user = self.request.user
        if user.is_staff:
            return FullProfileSerializer
        return BasicProfileSerializer

    def get_serializer(self, instance=None, *args, **kwargs):

        if hasattr(instance, 'user'):
            user = self.request.user

            if instance.user == user or user.is_staff:
                kwargs['instance'] = instance
                kwargs['context'] = self.get_serializer_context()
                return FullProfileSerializer(*args, **kwargs)

        return super(ProfileViewSet, self).get_serializer(
            instance, *args, **kwargs)

This doesn't work for the list view, however, as that one provides the get_serializer method with a Django Queryset object in place of an actual instance.
I'd still like to see this behaviour in a list view, i.e. when serializing many objects, so if anyone knows a more elegant way to do this that also covers the list view I'd much appreciate your answer.

Upvotes: 0

Related Questions