Search code examples
pythondjangoapidjango-rest-frameworkdjango-rest-viewsets

How to get data from ModelViewSet in DRF without calling an API call


I'm going to convert all my APIs into gRPC calls. At the moment I was able to transfer all the ViewSet into gRPC(sample code added end of this question). But ModelViewSet, it get an error like this.

Traceback (most recent call last):
  File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/venv/lib/python3.6/site-packages/grpc/_server.py", line 435, in _call_behavior
    response_or_iterator = behavior(argument, context)
  File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/servicers/tenant/main.py", line 15, in get_tenant
    data = ClientViewSet().list(request=original_request, )
  File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/common/lib/decorators.py", line 128, in wrapper_format_response
    final_data = call_func(func, self, request, transaction, exception, *args, **kwargs)
  File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/common/lib/decorators.py", line 99, in call_func
    return func(self, request, *args, **kwargs)
  File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/api_v1/viewsets.py", line 471, in list
    data = super().list(request, *args, **kwargs).data
  File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/venv/lib/python3.6/site-packages/rest_framework/mixins.py", line 38, in list
    queryset = self.filter_queryset(self.get_queryset())
  File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/venv/lib/python3.6/site-packages/rest_framework/generics.py", line 158, in filter_queryset
    queryset = backend().filter_queryset(self.request, queryset, self)
AttributeError: 'ClientViewSet' object has no attribute 'request'

So my viewsets.py look like this (format_response decorator convert it to Response object)

class ClientViewSet(viewsets.ModelViewSet):
    queryset = Client.objects.all()
    serializer_class = ser.ClientSerializer

    @format_response(exception=False)
    def list(self, request, *args, **kwargs):
        data = super().list(request, *args, **kwargs).data
        # data = {}
        return data, True, HTTPStatus.OK, 'data retrieve successfully'

When I call this as an API, it works perfectly. But I want to do the same thing without calling an API. Here how I was solving it,

from django.http import HttpRequest
from rest_framework.request import Request


# creating request object
django_request = HttpRequest()
django_request.method = 'GET'
drf_request = Request(django_request)

data = ClientViewSet().list(request=drf_request)
print(f'data: {data.data}')

The problem with the super() function in the ClientViewSet, but if I uncomment data = {} and comment out the calling super() function, it works both API and the above method. I go through inside the DRF code base where error occurred, when calling the API the self object has the request object and above method it doesn't exist.


Solution

  • In short, you need to call as_view() on the viewset and map the http verbs, and then call that function with a proper HttpRequest.

    All views in django end up being functions, DRF is sugar on top of that. A "ViewSet" doesn't exist in the normal way, as a standalone class, after routing is complete.

    django_request = HttpRequest()
    django_request.method = 'GET'
    
    my_view = ClientViewSet.as_view({'get': 'list', 'post':'create'})
    data = my_view(request=django_request)
    print(f'data: {data.data}')
    

    If you want the detail routes (users/37, ...), then you need to create a new view function mapping to those viewset functions. This can't be the same view function because http get now needs to point to a different function on the viewset than in the list case. See routers.py source for whats going on and what is mapped where.

    # map http 'get' to the 'retrive' function on the viewset
    my_view = ClientViewSet.as_view({'get': 'retrieve', ...})
    
    # pass in the kwarg the URL routing would normally extract
    client_pk = 9329032 
    data = my_view(request=django_request, pk=client_pk)
    

    If you want to see what all the mappings for your viewset are, then you an print them out using this snippet:

    router = SimpleRouter()
    router.register("client", ClientViewSet, basename="client")
    for url in router.urls:  # type: URLPattern
        print(f"{url.pattern} ==> {url.callback}")
        for verb, action in url.callback.actions.items():
            print(f"    {verb} -> {action}")
    
    # output will be something like this
    ^client/$ ==> <function ClientViewSet at 0x11b91c280>
        get -> list
    ^client/(?P<pk>[^/.]+)/$ ==> <function ClientViewSet at 0x11b91c3a0>
        get -> retrieve
        put -> update
        patch -> partial_update
        delete -> destroy
    

    The kwargs you pass to this view will depend on the settings in your ViewSet for things like lookup_url_kwarg, but in most cases they will be simple.