Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Register multiple routes in Django DRF - using and calling methods in ModelViewSets or Generics

I want to understand how to use Django DRF to register:

  • two different endpoints, and
  • only one endpoint making use of custom fields

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.

Flask - example endpoints to get a list of objects,last object

option 1

@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

option 2

@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.


Django DRF - first approach (ModelViewSet)

# 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 ?

Django DRF - second approach (Generics)

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 ?

Django DRF - Third attempt (ModelViewSet with basename)

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:

  • use a viewSet to handle lists, and expose a method to retrieve an item from the list
  • use two separate views, one for a list, and one for an item
  • map two distinct endpoints in my router from one single view or from two separate views
  • map one endpoint with field options to my router , from one single view
like image 432
user305883 Avatar asked Nov 23 '19 11:11

user305883


2 Answers

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.

like image 123
heemayl Avatar answered Sep 18 '22 16:09

heemayl


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/

  • The 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 endpoint
like image 28
Fran Avatar answered Sep 17 '22 16:09

Fran