[PYTHON] Implement hierarchical URLs with drf-nested-routers in Django REST framework

For example, when you want to get a list of items that belong to a specific category, you want to make the URL look like / categories / <category> / items / drf-nested-routers. alanjds / drf-nested-routers) I tried using it.

Try to implement with drf-nested-routers

While looking at README, implement a nested URL so that the Item comes under a specific Category like the URL below.

/categories
/categories/{pk}
/categories/{category_pk}/items
/categories/{category_pk}/items/{pk}

models.py First, implement the category and item model.

class Category(models.Model):
    name = models.CharField(max_length=30)
    slug = models.SlugField(unique=True)


class Item(models.Model):
    name = models.CharField(max_length=100)
    category = models.ForeignKey(Category)
    display_order = models.IntegerField(default=0, help_text='Display order')

serializers.py It also implements category and item serializers.

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = (
            'pk',
            'name',
            'slug',
        )


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

views.py As will be described later, although the router is used to generate the url, it cannot be specified so that pk and category_pk are numbers. So you have to be careful that if you inadvertently enter something like / category / hoge / items or / category / 1 / items / hoge, you will get a Value Error and an Internal Server Error.

To avoid ValueError,retrieve ()uses rest_framework.generics.get_object_or_404 instead of django.shortcuts.get_object_or_404. However, unfortunately list () does not exist rest_framework.generics.get_list_or_404, so if TypeError and ValuError occur following rest_framework.generics.get_object_or_404, raise Http404.

from rest_framework.generics import get_object_or_404

class CategoryViewSet(viewsets.ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer


class ItemViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all()

    def retrieve(self, request, pk=None, category_pk=None):
        item = get_object_or_404(self.queryset, pk=pk, category__pk=category_pk)
        serializer = self.get_serializer(item)
        return Response(serializer.data)

    def list(self, request, category_pk=None):
        try:
            items = get_list_or_404(self.queryset, category__pk=category_pk)
        except (TypeError, ValueError):
            raise Http404
        else:
            serializer = self.get_serializer(items, many=True)
            return Response(serializer.data)

urls.py Route using NestedSimpleRouter.

from rest_framework_nested import routers

router = routers.SimpleRouter(trailing_slash=False)
router.register(r'categories', CategoryViewSet)

categories_router = routers.NestedSimpleRouter(
    router, r'categories', lookup='category', trailing_slash=False)
categories_router.register(r'items', ItemViewSet)

urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^', include(categories_router.urls)),
]

The URL registered in the above implementation is as follows.

categories$ [name='category-list']
categories/(?P<pk>[^/.]+)$ [name='category-detail']
categories/(?P<categoary_pk>[^/.]+)/items$ [name='item-list']
categories/(?P<categoary_pk>[^/.]+)/items/(?P<pk>[^/.]+)$ [name='item-detail']

Implementation of update method

Looking at README, there was no sample implementation other than list () and retrieve () in the ViewSet on the child side. So I tried it.

create There are two points to be aware of. The first point is to ignore the existence of 'category' in the request data received from the client and use category_pk (or play with an error). The second point is to remember to check the existence of category. It looks like this when implemented while paying attention to them.

    def create(self, request, category_pk=None):
        category = get_object_or_404(Category.objects, pk=category_pk)
        request.data['category'] = category.pk
        return super(ItemViewSet, self).create(request)

update Basically, be careful as with create. However, the processing when the category is specified in the data received from the client is annoying, but it is played by validation.

views.py:

    def update(self, request, category_pk=None, *args, **kwargs):
        category = get_object_or_404(Category.objects, pk=category_pk)
        request.data['category'] = category.pk
        return super(ItemViewSet, self).update(request, *args, **kwargs)

serializers.py:

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

    def validate_category(self, category):
        if self.instance and self.instance.category_id != category.id:
            raise serializers.ValidationError("can not update to category.")

        return category

I think it would be nice to be able to change the category, but I wonder if the status code in that case is around status.HTTP_204_NO_CONTENT. The resource does not exist in the URL when PUT is done.

destroy Basically, destroy is the same as create. However, since it is not necessary to get the category, implement it so that the item is obtained with get_object_or_404.

    def destroy(self, request, pk=None, category_pk=None):
        item = get_object_or_404(self.queryset, pk=pk, category__pk=category_pk)
        self.perform_destroy(item)
        return Response(status=status.HTTP_204_NO_CONTENT)

Sort children

It's easy to sort the results of accessing / categories / {category_pk} / items. Just modify the ViewSet query_set.

class ItemViewSet(viewsets.ModelViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all().order_by('display_order')

However, with the above method, it is not possible to put items in the result of / categories / {category_pk} and sort them. In this case, there seems to be no other way but to specify it in Model ordering.

serializers.py:

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = (
            'pk',
            'name',
            'slug',
            'item_set'
        )

    item_set = ItemSerializer(many=True, read_only=True)

models.py:

class Item(models.Model):
    class Meta(object):
        ordering = ('display_order', 'pk')

    name = models.CharField(max_length=100)
    category = models.ForeignKey(Category)
    display_order = models.IntegerField(default=0, help_text='Display order')

Use slug for URL

If you want categories / some-slug / items instead of categories / 1 / items, just specify lookup_field in ViewSet.

class CategoryViewSet(viewsets.ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer
    lookup_field = 'slug'

The URL registered in this case is as follows.

categories$ [name='category-list']
categories/(?P<slug>[^/.]+)$ [name='category-detail']
categories/(?P<category_slug>[^/.]+)/items$ [name='item-list']
categories/(?P<category_slug>[^/.]+)/items/(?P<pk>[^/.]+)$ [name='item-detail']

Since the argument passed to ItemViewSet is category_slug, the arguments of various methods need to match it.

class ItemViewSet(viewsets.ModelViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all().order_by('display_order')

    def retrieve(self, request, pk=None, category_slug=None):
        item = get_object_or_404(self.queryset, pk=pk, category__slug=category_slug)
        serializer = self.get_serializer(item)
        return Response(serializer.data)

list_ruote list_route can also be used normally. For example, if you want to sort items in the specified order, implement a method to update display_order in categories / 1 / items / sort.

class ItemViewSet(viewsets.ModelViewSet):
    (...abridgement)

    @list_route(methods=['patch'])
    @transaction.atomic
    def sort(self, request, category_pk=None):
        items = self.queryset.filter(category__pk=category_pk)

        for i, pk in enumerate(request.data.get('item_pks', [])):
            items.filter(pk=pk).update(display_order=i + 1)

        return Response()

Source code

If you feel like it, raise the sample code somewhere.

Various versions

Python==3.6 Django==1.10.6 djangorestframework==3.6.2 drf-nested-routers==0.90.0

Reference site

https://github.com/alanjds/drf-nested-routers

Recommended Posts

Implement hierarchical URLs with drf-nested-routers in Django REST framework
Implement JWT login functionality in Django REST framework
Implementing authentication in Django REST Framework with djoser
Django REST framework with Vue.js
Login with django rest framework
[Django] Use MessagePack with Django REST framework
Create RESTful APIs with Django Rest Framework
Logical deletion in Django, DRF (Django REST Framework)
CRUD GET with Nuxt & Django REST Framework ②
CRUD POST with Nuxt & Django REST Framework
CRUD GET with Nuxt & Django REST Framework ①
Implementation of custom user model authentication in Django REST Framework with djoser
How to deal with garbled characters in json of Django REST Framework
CRUD PUT, DELETE with Nuxt & Django REST Framework
Django REST framework basics
Django Rest Framework Tips
Create a Todo app with Django REST Framework + Angular
More new user authentication methods with Django REST Framework
Create a Todo app with the Django REST framework
Create APIs around user authentication with Django REST Framework
When you want to filter with Django REST framework
List method for nested resources in Django REST framework
Implement APIs at explosive speed using Django REST Framework
Implement follow functionality in Django
Django REST framework stumbling block
How to write custom validations in the Django REST Framework
reload in django shell with ipython
Implementation of JWT authentication functionality in Django REST Framework using djoser
Quickly implement REST API in Python
GraphQL API with graphene_django in Django
Create a REST API to operate dynamodb with the Django REST Framework
ng-admin + Django REST framework ready-to-create administration tool
Miscellaneous notes about the Django REST framework
Django REST Framework + Clean Architecture Design Consideration
Implement a Custom User Model in Django
I want to create an API that returns a model with a recursive relationship in the Django REST Framework
How to get people to try out django rest framework features in one file
How to implement Rails helper-like functionality in Django
Save multiple models in one form with Django
Start Django in a virtual environment with Pipenv
Build a Django environment with Vagrant in 5 minutes
How to implement "named_scope" of RubyOnRails with Django
Remove extra strings in URLs with regular expressions
Django REST framework A little useful to know.
Configure a module with multiple files in Django
How to create a Rest Api in Django
Sometimes you want to access View information from Serializer with DRF (Django REST Framework)