I want to understand how to use Django DRF to register:
Please show the differences in using ViewSets and Generics.
These are my attempts.
I came from Flask, and found very clear to define multiple end-points with decorators.
@application.route('/people/', endpoint='people')
def people():
# return a list of people
pass
@application.route('/last/', endpoint='last_person')
def last_person():
# return last person
pass
@application.route('/people/', endpoint='people')
def people():
field = request.args.get('last', None)
if field:
# return last person from list of people
else:
# return list of people
I understood the benefit of DRF may be consistency and read the documentation, but found cumbersome and wish to understand more clearly how to make use of ModelsViewSets and Generics to see the benefits VS flask.
Please help with an example to fetch a list of users, and last user.
# serializer.py
from rest_framework import serializers
from .models import Person
class PersonSerializer( serializers.HyperlinkedModelSerializer):
class Meta:
model = Person
fields = ('name', 'nickname', 'timestamp')
# views.py
class PersonViewSet( viewsets.ModelViewSet):
queryset = Person.objects.all().order_by('name')
serializer_class = PersonSerializer
I understood the class PersonViewSet
should be ready to expose method to both fetch a list, as well as retrieve an item, because of ModelViewSet
:
https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset
But how to explicitly see that when registering the endpoints ?
Example:
# urls.py
router = routers.DefaultRouter()
router.register(r'people', views.PersonViewSet)
app_name = 'myapi'
urlpatterns = [
path('', include(router.urls)),
]
When I browse http://127.0.0.1:8000/api/
, I can see only one endpoint.
Yes, if I try http://127.0.0.1:8000/api/1/
it will retrieve a user by itself, but how can I read it in the code above?
how can I map a method from PersonViewSet
(something like PersonViewSet.get_last_person()
) to instruct using the same endpoint to get last entry ?
I read Generics
models exposes APIs models suited to retrieve one single item, rather than a list:
https://www.django-rest-framework.org/api-guide/generic-views/#retrieveapiview
I tried to add, in views.py
:
# I make use of RetrieveAPIView now
class LastPersonView( generics.RetrieveAPIView):
queryset = Person.objects.all()
serializer_class = PersonSerializer
and in urls.py
:
router.register(r'people', views.PersonViewSet)
router.register(r'last-person', views.LastPersonView)
But that will yield error: AttributeError: type object 'LastPersonView' has no attribute 'get_extra_actions because Generics does not have get_extra_actions , like instead ViewSet
How to register both views in my router, in this second example ?
https://stackoverflow.com/a/40759051/305883
I could also instruct a ViewSet to register different endpoints in my urls.py
, by assigning a basename:
router.register(r'people', views.PersonViewSet)
router.register(r'last-person', views.PersonViewSet, basename='lastperson')
and use the same ModelViewSet
:
class PersonViewSet( viewsets.ModelViewSet):
queryset = Person.objects.all().order_by('name')
serializer_class = PersonSerializer
I understood the benefit of this approach is more "simple", because queryset is always the same.
But I can I register a method to retrieve the last object, and map that method in my router (include(router.urls)
) ?
Could offer examples making use of ModelViewSets, Generics and a more explicit approach that declare methods in Views and call those methods in an endpoint ?
Could you illustrate which approach may be better:
I'm going to go through each of the approaches you've taken and will provide a solution to meet your need to be based on that very approach. So let's roll ...
Your first approach:
Your first approach uses a very typical DRF setup with routes being generated for various actions for PersonViewSet
by DRF itself.
Now, you need to add a new URL endpoint that will resolve to the last object of the queryset
:
Person.objects.all().order_by('name')
and presumably, the endpoint should reside under the person
basename.
We can leverage the action
decorator in here to map HTTP GET on a certain URL to be mapped to a method in the viewset and from that method we can set the kwargs
attribute of the viewset instance to set pk
to that of the last object, and finally send the request to the retrieve
method itself e.g.:
from rest_framework.decorators import action
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all().order_by('name')
serializer_class = PersonSerializer
@action(
methods=['get'],
detail=False,
url_path='last',
url_name='last',
)
def get_last_person(self, request, *args, **kwargs):
last_pk = self.queryset.all().last().pk
self.kwargs.update(pk=last_pk)
return self.retrieve(request, *args, **kwargs)
Now if you request on the /people/last/
endpoint you would get the response for the last retrieved object.
Note that, if you have lookup_url_kwargs
and lookup_field
different than pk
, you need to use set those in kwargs
as you can imagine. Also, you can put the operations for retrieve
yourself rather than delegating to retrieve
but let's keep it DRY.
FWIW, if you want to have this endpoint for PUT/PATCH/DELETE as well, you need to add the methods in action
and delegate them to respective actions based on the method name.
Your second approach:
Views are not ViewSets (check out the base classes of both: rest_framework.views.View
and rest_framework.viewsets.ViewSet
); DRF routers create endpoints for viewsets, not views. There are ways to turn views into viewsets, basically just by inheriting from viewsets.GenericViewSet
-- which it turn inherits from viewsets.ViewSetMixin
and generics.GenericAPIView
.
viewsets.ViewSetMixin
has the actual magic that turns views into viewsets by providing all the necessary action-method mapping in the as_view
classmethod.
To make this approach to work, we need to define a retrieve
method that sends the response from the serializer:
from rest_framework import generics
from rest_framework.response import Response
class LastPersonView(generics.RetrieveAPIView):
serializer_class = PersonSerializer
queryset = Person.objects.all().order_by('name')
def retrieve(self, request, *args, **kwargs):
instance = self.queryset.all().last()
serializer = self.get_serializer(instance)
return Response(serializer.data)
generics.RetrieveAPIView
's get
method delegates the request to the retrieve
method hence we've overridden retrieve
here.
Now, we need to define the route as a regular endpoint, not in a DRF router:
urlpatterns = router.urls + [
path('last-person/', LastPersonView.as_view()),
]
Your third approach:
You've registered two different prefixes for the same viewset (again using view here wouldn't work, must be a viewset), so two different set of URL mappings with all the same CRUD operations. I don't think this is not what you want based on your expectations so I'm not going to discuss this approach further but I think you get the idea from above why it's not relevant.
Now, if I have to choose, I would prefer the first approach as everything is under the same viewset, prefix and basename, and you don't need to muck around with the URLConf.
I hope the above clarifies a thing or two.
First of all, you can accomplish what you do in flask creating a list_route
o a detail_route
through the @action
decorator in your ViewSet
. The router will create for you the endpoint based on the name of the function. For example
class PersonViewSet( viewsets.ModelViewSet):
queryset = Person.objects.all().order_by('name')
serializer_class = PersonSerializer
@action(detail=True, methods=['post']) # detail route
def set_password(self, request, pk=None):
# your code
@action(detail=False) # list route
def last_person(self, request):
# your code
this code will create a people/last_person
and a people/<id>/set_password
For more information about how the router creates the paths: https://www.django-rest-framework.org/api-guide/routers/#routing-for-extra-actions
Using this extra action allow you to have different serializer for different actions and so have different fields exposed.
Here some clarification about your doubts:
A ModelViewSet
is only a "shortcut" that you can use when you want to expose a complete CRUD interface for your model. It's based on the GenericsViewSet
and other mixins and you have the chance to write a very little amount of code to make it work appropriately. If you don't have a model, then you should use the ViewSet
to build your API. If you only want to expose some operations, for example, only list or retrieve, you can build-up your API using the mixins, in this case mixins.ListModelMixin
and mixins.RetrieveModelMixin
and the GenericViewSet
Instead, the generics that you're using are only View
, not ViewSet
. This means that you cannot register them through the router. You should add them directly to the Django URLs, like any other view. As per drf documentation:
Django REST framework allows you to combine the logic for a set of related views in a single class, called a ViewSet.
Doc to viewset https://www.django-rest-framework.org/api-guide/viewsets/, doc to view https://www.django-rest-framework.org/api-guide/views/
basename
doesn't prevent you from writing two different ViewSet
. If you use the same ViewSet
you'll have the same functionality at two different endpointIf you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With