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.
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']
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)
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')
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()
If you feel like it, raise the sample code somewhere.
Python==3.6 Django==1.10.6 djangorestframework==3.6.2 drf-nested-routers==0.90.0
https://github.com/alanjds/drf-nested-routers
Recommended Posts