11 Viewsets - Routers
11 Viewsets - Routers
Blango Repo
In the Terminal
Clone the repo. Your command should look something like this:
Viewsets
Django Rest Framework has one more trick up its sleeve in regards to
reducing the amount of code to write: viewsets. This allows you to define a
single class which will handle both the list and detail API for a given model.
You can implement all, or just some of these, depending on what you want
your viewset to be able to support. Each method should return a
rest_framework.response.Response instance (like what we used when we
first built our function-based DRF API.
For example, here’s a viewset that allows listing all Tag objects in Blango
(assuming we’ve written a TagSerializer class already).
class TagViewSet(viewsets.ViewSet):
def list(self, request):
queryset = Tag.objects.all()
serializer = TagSerializer(queryset, many=True)
return Response(serializer.data)
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
tag_list = TagViewSet.as_view({
"get": "list",
"post": "create"
})
tag_detail = TagViewSet.as_view({
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy"
})
Routers
A DRF Router will inspect a viewset and determine what endpoints it has
available, then create the right URL patterns automatically. Usually you will
want to use a rest_framework.routers.DefaultRouter class. Also provided
is rest_framework.routers.SimpleRouter, but we’ll come back to the
differences in a moment.
router = DefaultRouter()
2. Register viewsets using a prefix. The prefix will be used as the initial
component of the path. For example to register our TagViewSet under
the tags path:
router.register("tags", TagViewSet)
3. Access the generated URL patterns with the urls attribute on the router.
These can be appended to urlpatterns or used with the include()
function. For example:
urlpatterns += [
# other patterns
path("", include(router.urls)),
]
We can actually register() multiple ViewSets and all the URL patterns will
be available in the urls attribute.
Try It Out
We’ve seen how easy it is to set up API endpoints using viewsets and
routers, so let’s now set up the Tag API in Blango.
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = "__all__"
Open api/views.py
And make sure you’re importing the new TagSerializer class and the Tag
model:
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
Now we’ll make use of the router. Open blog/api/urls.py and import the
DefaultRouter class:
Open api/urls.py
router = DefaultRouter()
router.register("tags", TagViewSet)
urlpatterns += [
path("auth/", include("rest_framework.urls")),
# ... other patterns omitted
path("", include(router.urls)),
]
That’s it. Load up the /api/v1/tags/ path in a browser or Postman, and you
should see a list of the tags in the system. If you like, you can set up the
various routes in Postman to try creating, editing and deleting them too.
View Blog
Customizing Viewsets
Customizing viewsets
Could we convert our other views to viewsets? We wouldn’t gain much
from converting the UserDetail view as it’s just a single class anyway, and
we would have to write extra code to disable most of the functionality. But
we can convert our Post views to viewsets. We’ll just need to handle having
a different serializer for the list and detail methods.
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [AuthorModifyOrReadOnly |
IsAdminUserForObject]
queryset = Post.objects.all()
def get_serializer_class(self):
if self.action in ("list", "create"):
return PostSerializer
return PostDetailSerializer
This class can replace both our Post related views, and we can remove the
URL patterns too, as they’ll be automatically added using the router.
Try It Out
Let’s refactor our Post API down to a single viewset. In blog/api/views.py
add the PostViewSet class:
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [AuthorModifyOrReadOnly |
IsAdminUserForObject]
queryset = Post.objects.all()
def get_serializer_class(self):
if self.action in ("list", "create"):
return PostSerializer
return PostDetailSerializer
Open api/urls.py
Then remove the URL pattern for them, these two lines:
Your initial urlpatterns creation should now just contain the map to the
UserDetail view.
urlpatterns = [
path("users/<str:email>", UserDetail.as_view(),
name="api_user_detail"),
]
router.register("posts", PostViewSet)
Jump into Postman or load the API in the browser and try it out. You should
notice that the Posts API behaves the same as it did before, however we’ve
managed to reduce the amount of code that we need to write. This also
helps ensures consistency when it comes to building and naming the URLs.
View Blog
On that note, how are the URLs that a router generates, named? DRF uses
the name of model from the queryset, then the suffixes -list and -detail
are added. For example, our “post detail” view now has the name post-
detail, rather than api_post_detail as it did before. We could customize
this by provided a basename string as the third argument to the register()
method.
Extra Viewset Actions
Let’s look at an example and then discuss it in more detail. We’ll create an
extra method on the TagViewSet that allows us to get a list of all Post objects
that have the specific Tag. Here’s how it’s implemented.
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
We have access to the pk from the URL, so we could fetch the Tag object
from the database ourself. However, the ModelViewSet class provides a
helper method that will do that for us – get_object() – so we use that
instead.
Then we use the PostSerializer to serialize the many Post objects on
tag.post. Since PostSerializer users a HyperlinkRelatedField it needs
access to the current request so we need to pass that in a context
dictionary. Then we just return a DRF Response with the serialized data.
methods: A list of HTTP methods that the action will respond to. Defaults
to ["get"].
detail: Determines if the action should apply to detail requests (if True)
or list (if False). This argument is required.
url_path: Manually specify the path to be used in the URL. Defaults to
the method name (e.g. posts).
url_name: Manually specify the name of the URL pattern. Defaults to the
method name with underscores replaced by dashes. The full name of
our method’s URL is tag-posts.
name: A name to display in the Extra Actions menu in the DRF GUI.
Defaults to the name of the method.
Try It Out
Let’s implement the posts method on your version of Blango. Open
blog/api/views.py. Start by adding the new imports at the top of the file:
class TagViewSet(viewsets.ModelViewSet):
# existing attributes omitted
Those are the only changes we need to make, the router will automatically
read the new method and generate the URL pattern for us. Load up a Tag
detail view in DRF in a browser and you’ll see the Extra Actions menu. You
can then click Posts with this Tag to go to the posts list for the tag.
View Blog
extra actions
Viewsets and routers seem great, but what are the drawbacks of them
compared to views?
Viewsets vs views
Viewsets do provide a useful abstraction and can be a really quick way of
building an API, however as with any type of “automatic” or “one size fits
all” approach, they may not always do what you want. We decided not to
convert our user API to a viewset as we did not require all the functionality
it would have given us, and in fact we would have to write extra code to
strip it out. So as usual, it comes down to the individual case to decide
whether to use a viewset or not.
If you’re unsure, check out the viewsets documentation for a more in depth
look at how to customize viewsets to further suit your needs.
Wrap up
That’s the end of the module and indeed the whole course. We continue our
look at Django Rest Framework in the next course, starting with how to test
DRF views.
Pushing to GitHub
Pushing to GitHub
Before continuing, you must push your work to GitHub. In the terminal:
git add .
git commit -m "Finish viewsets and routers"
Push to GitHub:
git push