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.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments