'How can I make a Django REST framework /me/ call?

Suppose I have a ViewSet:

class ProfileViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows a user's profile to be viewed or edited.
    """
    permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

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

...and a HyperlinkedModelSerializer:

class ProfileSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Profile
        read_only_fields = ('user',)

I have my urls.py set up as:

router.register(r'profiles', api.ProfileViewSet, base_name='profile')

This lets me access e.g. /api/profile/1/ fine.

I want to set up a new endpoint on my API (similar to the Facebook API's /me/ call) at /api/profile/me/ to access the current user's profile - how can I do this with Django REST Framework?



Solution 1:[1]

You could create a new method in your view class using the list_route decorator, like:

class ProfileViewSet(viewsets.ModelViewSet):

    @list_route()
    def me(self, request, *args, **kwargs):
        # assumes the user is authenticated, handle this according your needs
        user_id = request.user.id
        return self.retrieve(request, user_id)

See the docs on this for more info on @list_route

I hope this helps!

Solution 2:[2]

Using the solution by @Gerard was giving me trouble:

Expected view UserViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the .lookup_field attribute on the view correctly..

Taking a look at the source code for retrieve() it seems the user_id is not used (unused *args)

This solution is working:

from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404

from rest_framework import filters
from rest_framework import viewsets
from rest_framework import mixins
from rest_framework.decorators import list_route
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from ..serializers import UserSerializer


class UserViewSet(viewsets.ModelViewSet):
    """
    A viewset for viewing and editing user instances.
    """
    serializer_class = UserSerializer
    User = get_user_model()
    queryset = User.objects.all()
    filter_backends = (filters.DjangoFilterBackend, filters.SearchFilter)
    filter_fields = ('username', 'email', 'usertype')
    search_fields = ('username', 'email', 'usertype')

    @list_route(permission_classes=[IsAuthenticated])
    def me(self, request, *args, **kwargs):
        User = get_user_model()
        self.object = get_object_or_404(User, pk=request.user.id)
        serializer = self.get_serializer(self.object)
        return Response(serializer.data)

Accessing /api/users/me replies with the same data as /api/users/1 (when the logged-in user is user with pk=1)

Solution 3:[3]

You can override the get_queryset method by filtering the queryset by the logged in user, this will return the logged in user's profile in the list view (/api/profile/).

def get_queryset(self):        
    return Profile.objects.filter(user=self.request.user)

or

def get_queryset(self):
    qs = super(ProfileViewSet, self).get_queryset()
    return qs.filter(user=self.request.user)

or override the retrieve method like so, this will return the profile of the current user.

def retrieve(self, request, *args, **kwargs):
    self.object = get_object_or_404(Profile, user=self.request.user)
    serializer = self.get_serializer(self.object)
    return Response(serializer.data)

Solution 4:[4]

From Gerard's answer and looking at the error pointed out by delavnog, I developed the following solution:

class ProfileViewSet(viewsets.ModelViewSet):

    @list_route(methods=['GET'], permission_classes=[IsAuthenticated])        
    def me(self, request, *args, **kwargs):
        self.kwargs.update(pk=request.user.id)
        return self.retrieve(request,*args, **kwargs)

Notes:

  • ModelViewSet inherits GenericAPIView and the logic to get an object is implemented in there.
  • You need to check if the user is authenticated, otherwise request.user will not be available. Use at least permission_classes=[IsAuthenticated].
  • This solution is for GET but you may apply the same logic for other methods.
  • DRY assured!

Solution 5:[5]

Just override the get_object()

eg.

def get_object(self):
    return self.request.user

Solution 6:[6]

Just providing a different way. I did it like this:

def get_object(self):
    pk = self.kwargs['pk']
    if pk == 'me':
        return self.request.user
    else:
        return super().get_object()

This allows other detail_routes in the ViewSet to work like /api/users/me/activate

Solution 7:[7]

I've seen quite a few fragile solutions so I thought I'll respond with something more up-to-date and safer. More importantly you don't need a separate view, since me simply acts as a redirection.

    @action(detail=False, methods=['get', 'patch'])
    def me(self, request):
        self.kwargs['pk'] = request.user.pk

        if request.method == 'GET':
            return self.retrieve(request)
        elif request.method == 'PATCH':
            return self.partial_update(request)
        else:
            raise Exception('Not implemented')

It's important to not duplicate the behaviour of retrieve like I've seen in some answers. What if the function retrieve ever changes? Then you end up with a different behaviour for /me and /<user pk>

If you only need to handle GET requests, you could also use Django's redirect. But that will not work with POST or PATCH.

Solution 8:[8]

Considering a OneToOneField relationship between the Profile and the User models with related_name='profile', I suggest the following as the @list_route has been deprecated since DRF 3.9

class ProfileViewSet(viewsets.GenericViewSet):
    serializer_class = ProfileSerializer

    @action(methods=('GET',), detail=False, url_path='me', url_name='me')
    def me(self, request, *args, **kwargs):
        serializer = self.get_serializer(self.request.user.profile)
        return response.Response(serializer.data)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Gerard
Solution 2 blueFast
Solution 3
Solution 4 Community
Solution 5 Michael Henry
Solution 6 sanfilippopablo
Solution 7
Solution 8 Dos