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

user305883

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
heemayl

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.

edited at
0

Comments

0 comments
Login to comment

Related

From Dev

DRF - django_filters -Using custom methods

From

Register multiple routes using range for loop slices/map

From Dev

Generics in Java preventing calling of methods

From Java

Using generics for similar methods

From Dev

Django DRF - using basic search filter to do an multiple OR search

From Java

Calling multiple methods in Java

From Dev

Calling multiple methods on a variable

From Dev

Api routes for multiple get methods

From Dev

Using filters with list_routes in drf

From Dev

Register multiple admin to django

From Dev

Implementing And and OR methods in Bifunctions using Generics

From Dev

Refactoring methods using Java generics

From Dev

Django rest framework modelviewsets custom response

From Dev

Testing multiple methods calling order

From Dev

Calling multiple methods of an object in sequence

From Dev

Django calling methods from models

From Dev

Using When and Less then in Django DRF?

From Python

Using Django-filter with DRF

From Dev

Calling super() to parent class using generics

From Java

Java Generics - calling specific methods from generic-typed ones

From Dev

Faster calling of methods using reflection

From Dev

Calling methods using char** with Cython

From Java

Calling methods reflectivly or using fixed methods with inheritance?

From Dev

Overriding abstract methods when using generics

From Dev

Refactor methods into a Single Method by using typescript Generics

From Dev

Using generics keys for methods returning hashmaps

From Dev

Haskell Servant: How to prefix routes when using generics?

From Dev

Using unity xml config to register an interface with nested generics

From Dev

DRF Register a sub viewset

Related Related

HotTag

Archive