3
votes

I'm making a REST API with Django Rest Framework (DRF) which has the following endpoints:

/users/
/users/<pk>/
/items/
/items/<pk>/

but I'd like to add the endpoint:

/users/<pk>/items/

which would of course return the items that belong (have a foreign key) to that user.

Currently my code is:

#########################
##### myapp/urls.py #####
#########################

from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter
from rest_framework.decorators import api_view, renderer_classes
from rest_framework import response, schemas
from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer

from myapp.views import ItemViewSet, UserViewSet

# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'items', ItemViewSet)

@api_view()
@renderer_classes([OpenAPIRenderer, SwaggerUIRenderer])
def schema_view(request):
    generator = schemas.SchemaGenerator(title='My API')
    return response.Response(generator.get_schema(request=request))

urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

##########################
##### myapp/views.py #####
##########################

from django.contrib.auth import get_user_model
from rest_framework import viewsets, permissions 

from myapp.serializers import MyUserSerializer, ItemSerializer
from myapp.models import Item 


class UserViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = get_user_model().objects.all()
    serializer_class = MyUserSerializer
    permission_classes = (permissions.IsAuthenticated,)

class ItemViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer
    permission_classes = (permissions.IsAuthenticated,)

################################
##### myapp/serializers.py #####
################################

from rest_framework import serializers
from django.contrib.auth import get_user_model

from myapp.models import Item

class MyUserSerializer(serializers.ModelSerializer):
    class Meta:
        model = get_user_model()
        fields = ('pk', 'email',)

class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = Item
        fields = ('pk', 'name',)

Is there a good way to add this endpoint in DRF, given how I'm using DRF?

I could just add a function view in urls.py like so:

from myapp.views import items_for_user

urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^users/(?P<pk>[0-9]+)/items/$', items_for_user),
]

but I want to leverage DRF, get the browsable API, and make use of ViewSets instead of coding one-off function views like this.

Any ideas?

1

1 Answers

1
votes

Took me a while to figure this out. I've been using view sets, so I'll give this answer within that setting.

First, URLConf and registered routes remain unchanged, i.e.,

router = DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'items', ItemViewSet)

urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^api-auth/',
        include('rest_framework.urls', namespace='rest_framework')
    ),
]

Your items will still be at /items/<pk>/, with permissions crafted for each of them depending on who requests them, by creating custom permissions, for example:

class IsItemOwnerPermissions(permissions.DjangoObjectPermissions):
    """
    The current user is the owner of the item.
    """
    def has_object_permission(self, request, view, obj):
        # A superuser?
        if request.user.is_superuser:
            return True
        # Owner
        if obj.owner.pk == request.user.pk:
            return True
        return False

Next, for /user/<pk>/items/ you need to define @detail_route, like this:

class UserViewSet(viewsets.ReadOnlyModelViewSet):
    # Your view set properties and methods
    @detail_route(
        methods=['GET', 'POST'],
        permission_classes=[IsItemOwnerPermissions],
    )
    def items(self, request, pk=None):
        """
        Returns a list of all the items belonging to `/user/<pk>`.
        """
        user = get_user_model().objects.get(pk=pk)
        items = user.items.all()
        page = self.paginate_queryset(items)
        if page is None:
            serializer = ItemSerializer(
                objs, context={'request': request}, many=True
            )
            return Response(serializer.data)
        else:
            serializer = ItemSerializer(
                page, context={'request': request}, many=True
            )
            return self.get_paginated_response(serializer.data)

A detailed route named xyz corresponds to the route user/<pk>/xyz. There are also list routes (@list_route); one named xyz would correspond to user/xyz (for example user/add_item).

The above structure will give you: /user, /user/<pk>, user/<pk>/items, /items, and /items/<pk>, but not (as I wrongly tried to achieve) user/<user_pk>/items/<items_pk>. Instead, user/<pk>/items will give you a list of user's items, but their individual properties will still be accessible only via /items/<pk>.

I just got this to work on my project, and the above code is a quick adaptation to your case. I hope it helps you, but there might still be problems there.

Update: What you wanted can be done using custom hyperlinked fields. I didn't try it (yet), so I cannot say how to use these, but there are nice examples on that link.