More views and viewsets¶
A better URL structure¶
We have three API endpoints
/polls/
and/polls/<pk>/
/choices/
/vote/
They get the work done, but we can make our API more intuitive by nesting them correctly. Our redesigned urls look like this:
/polls/
and/polls/<pk>
/polls/<pk>/choices/
to GET the choices for a specific poll, and to create choices for a specific poll. (Idenitfied by the<pk>
)/polls/<pk>/choices/<choice_pk>/vote/
- To vote for the choice identified by<choice_pk>
under poll with<pk>
.
Changing the views¶
We will make changes to ChoiceList
and CreateVote
, because the /polls/
and /polls/<pk>
have not changed.
from rest_framework import generics
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer
# ...
# PollList and PollDetail views
class ChoiceList(generics.ListCreateAPIView):
def get_queryset(self):
queryset = Choice.objects.filter(poll_id=self.kwargs["pk"])
return queryset
serializer_class = ChoiceSerializer
class CreateVote(APIView):
serializer_class = VoteSerializer
def post(self, request, pk, choice_pk):
voted_by = request.data.get("voted_by")
data = {'choice': choice_pk, 'poll': pk, 'voted_by': voted_by}
serializer = VoteSerializer(data=data)
if serializer.is_valid():
vote = serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
And change your urls.py to a nested structure.
#...
urlpatterns = [
path("polls/<int:pk>/choices/", ChoiceList.as_view(), name="choice_list"),
path("polls/<int:pk>/choices/<int:choice_pk>/vote/", CreateVote.as_view(), name="create_vote"),
]
You can see the changes by doing a GET to http://localhost:8000/polls/1/choices/
, which should give you.
[
{
"id": 1,
"votes": [],
"choice_text": "Flask",
"poll": 1
},
{
"id": 2,
"votes": [
],
"choice_text": "Django",
"poll": 1
}
]
You can vote for choices 2, of poll 1 by doing a POST to http://localhost:8000/polls/1/choices/2/vote/
with data {"voted_by": 1}
.
{
"id": 2,
"choice": 2,
"poll": 1,
"voted_by": 1
}
Lets get back to ChoiceList
.
# urls.py
#...
urlpatterns = [
# ...
path("polls/<int:pk>/choices/", ChoiceList.as_view(), name="choice_list"),
]
# apiviews.py
# ...
class ChoiceList(generics.ListCreateAPIView):
def get_queryset(self):
queryset = Choice.objects.filter(poll_id=self.kwargs["pk"])
return queryset
serializer_class = ChoiceSerializer
From the urls, we pass on pk
to ChoiceList
. We override the get_queryset
method, to filter on choices with this poll_id
, and let DRF handle the rest.
And for CreateVote
,
# urls.py
#...
urlpatterns = [
# ...
path("polls/<int:pk>/choices/<int:choice_pk>/vote/", CreateVote.as_view(), name="create_vote"),
]
# apiviews.py
# ...
class CreateVote(APIView):
def post(self, request, pk, choice_pk):
voted_by = request.data.get("voted_by")
data = {'choice': choice_pk, 'poll': pk, 'voted_by': voted_by}
serializer = VoteSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
We pass on poll id and choice id. We subclass this from APIView
, rather than a generic view, because we competely customize the behaviour. This is similar to our earlier APIView
, where in we are passing the data to a serializer, and saving or returning an error depending on whether the serializer is valid.
Introducing Viewsets and Routers¶
Our urls are looking good, and we have a views with very little code duplication, but we can do better.
The /polls/
and /polls/<pk>/
urls require two view classes, with the same serializer and base queryset. We can group them into a viewset, and connect them to the urls using a router.
This is what it will look like:
# urls.py
# ...
from rest_framework.routers import DefaultRouter
from .apiviews import PollViewSet
router = DefaultRouter()
router.register('polls', PollViewSet, basename='polls')
urlpatterns = [
# ...
]
urlpatterns += router.urls
# apiviews.py
# ...
from rest_framework import viewsets
from .models import Poll, Choice
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer
class PollViewSet(viewsets.ModelViewSet):
queryset = Poll.objects.all()
serializer_class = PollSerializer
There is no change at all to the urls or to the responses. You can verify this by doing a GET to
/polls/
and /polls/<pk>/
.
Choosing the base class to use¶
We have seen 4 ways to build API views until now
- Pure Django views
APIView
subclassesgenerics.*
subclassesviewsets.ModelViewSet
So which one should you use when? My rule of thumb is,
- Use
viewsets.ModelViewSet
when you are going to allow all or most of CRUD operations on a model. - Use
generics.*
when you only want to allow some operations on a model - Use
APIView
when you want to completely customize the behaviour.
Next steps¶
In the next chapter, we will look at adding access control to our apis.