Django Rest Framework custom POST URL endpoints with defined parameter (request.POST) with Swagger or other doc
Asked Answered
B

2

12

previously in Django 1.11, I'ved defined Django REST API in this way:

in url.py

url(r'^api/test_token$', api.test_token, name='test_token'),

in api.py

@api_view(['POST'])
def test_token(request):
    # ----- YAML below for Swagger -----
    """
    description: test_token
    parameters:
      - name: token
        type: string
        required: true
        location: form       
    """
    token = request.POST['token']

    return Response("test_token success", status=status.HTTP_200_OK)

Now that I am migrating to Django 3.1.5, I'd like to know how the above can be achieved in the same way with Django Rest Framework (DRF). In the above particular case, it is POST API "test_token" that takes in one parameter. And generate API documentation like swagger/redoc (that can be used for testing the API)

enter image description here

Some Notes:

How can I implement this on Django 3.x ? (as in the title: Django Rest Framework custom POST URL endpoints with defined parameter with Swagger or other doc)

UPDATE:

I think there is some form of solution from here: https://github.com/tfranzel/drf-spectacular/issues/279

As I have a whole lot of APIs that uses @api_view, the changes docstring to the decorator @extend_schema may be simplest migration path. I hope someone can provide guidance on url.py to the conversion using @extend_schema. This is to get the url end points and swagger implemented. Thanks.

This is closest I'ved got with drf-spectacular however

@extend_schema( 
 parameters=[OpenApiParameter( 
     name='token', 
     type={'type': 'string'}, 
     location=OpenApiParameter.QUERY, 
     required=False, 
     style='form', 
     explode=False, 
 )], 
 responses=OpenApiTypes.OBJECT, 
) 
@api_view(['POST'])
def test_api(request):
    # ----- YAML below for Swagger -----
    """
    description: test_api
    parameters:
      - name: token
        type: string
        required: true
        location: form       
    """
    token = request.POST['token']
    
    return Response("success test_api:" + token, status=status.HTTP_200_OK)

its giving this (which is incorrect), notice the token query

curl -X POST "http://localhost:8000/api/test_token/?token=hello" -H  "accept: application/json" -H  "X-CSRFToken: JyPOSAQx04LK0aM8IUgUmkruALSNwRbeYDzUHBhCjtXafC3tnHRFsxvyg5SgMLhI" -d ""

instead of a POST input param (how to get this ?)

curl -X POST --header 'Content-Type: application/x-www-form-urlencoded' --header 'Accept: application/json' --header 'X-CSRFToken: aExHCSwrRyStDiOhkk8Mztfth2sqonhTkUFaJbnXSFKXCynqzDQEzcRCAufYv6MC' -d 'token=hello' 'http://localhost:8000/api/test_token/

THE SOLUTION:

url.py

from drf_yasg.utils import swagger_auto_schema 

from rest_framework.response import Response
from rest_framework import status

from rest_framework.decorators import parser_classes
from rest_framework.parsers import FormParser

token = openapi.Parameter('token', openapi.IN_FORM, type=openapi.TYPE_STRING, required=True)
something = openapi.Parameter('something', openapi.IN_FORM, type=openapi.TYPE_INTEGER, required=False)
@swagger_auto_schema(
    method="post",
    manual_parameters=[token, something],
    operation_id="token_api"
)
@api_view(['POST'])
# this is optional and insures that the view gets formdata
@parser_classes([FormParser])
def token_api(request):
    token = request.POST['token']
    something = request.POST['something']
    
    return Response("success test_api:" + token + something, status=status.HTTP_200_OK)


schema_view = get_schema_view(
   openapi.Info(
      title="Snippets API",
      default_version='v1',
      description="Test description",
      terms_of_service="https://www.google.com/policies/terms/",
      contact=openapi.Contact(email="[email protected]"),
      license=openapi.License(name="BSD License"),
   ),
   public=True,
   permission_classes=[permissions.AllowAny],
)


urlpatterns = [
    
    path('token_api', token_api, name='token_api'),

    path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
    
] + required_urlpatterns
Biotite answered 27/1, 2021 at 12:14 Comment(4)
You can look for drf-spectacular library. It is supported and well documented. It gives you swagger/redoc in package drf-spectacular.readthedocs.io/en/latest/readme.html And for you custom parameters, you can look here: drf-spectacular.readthedocs.io/en/latest/customization.html#Yesima
you should use re_path for regular expressions from Django 2.0. Change your url like this: from django.urls import re_path > re_path(r'^api/test_token$', api.test_token, name='test_token'),Zhdanov
@MaratAblayev thanks. Actually in your second link, might be applicable, since I use api_view, it says this, I wonder how would that work: "Many libraries use "api_view" or APIView instead of ViewSet or GenericAPIView. In those cases, introspection has very little to work with. The purpose of this extension is to augment or switch out the encountered view (only for schema generation). Simply extending the discovered class class Fixed(self.target_class) with a queryset or serializer_class attribute will often solve most issues."Biotite
@MaratAblayev I'ved UPDATED some info, its quite close using the decorator extend_schema from drf-spectacular , but unable to define it as an INPUT param for POSTBiotite
P
7

As you said django-rest-swagger is deprecated.

That's why it is recommended to use drf-yasg.

from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema    

class ArticleViewSet(viewsets.ModelViewSet):
    @swagger_auto_schema(request_body=openapi.Schema(
        type=openapi.TYPE_OBJECT, 
        properties={
            'test_token': openapi.Schema(type=openapi.TYPE_STRING, description='string'),
        }
    ))
    def create(self, request, *args, **kwargs):
        ...

Or if you want to use a DRF action

    @swagger_auto_schema(method="post", request_body=openapi.Schema(
        type=openapi.TYPE_OBJECT, 
        properties={
            'test_token': openapi.Schema(type=openapi.TYPE_STRING, description='string'),
        }
    ))
    @action(method=["post"], detail=False)
    def my_post_action(self, request, *args, **kwargs):
        ...

Or with api view:

# here we define that this view accepts a json (or object parameter) that has test_token parameter inside of it
@swagger_auto_schema(method='post', 
    request_body=openapi.Schema(
    type=openapi.TYPE_OBJECT, # object because the data is in json format
    properties={
        'test_token': openapi.Schema(type=openapi.TYPE_STRING, description='this test_token is used for...'),
    }
), operation_id="token_view")
# your view
@api_view(['POST'])
def token_view(request):
    pass

And your url.py will look like so

# define some basic info about your api for swagger
schema_view = get_schema_view(
   openapi.Info(
      title="Snippets API",
      default_version='v1',
      description="Test description",
      terms_of_service="https://www.google.com/policies/terms/",
      contact=openapi.Contact(email="[email protected]"),
      license=openapi.License(name="BSD License"),
   ),
   public=True,
   permission_classes=[permissions.AllowAny],
)

urlpatterns = [
    # define your api view url
    path('token_view/', token_view),
    # define the url of the swagger ui
    url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
]
Pola answered 29/1, 2021 at 14:11 Comment(20)
Im not sure how its done, I'ved tried to download the testproj and set it up. However, I am not sure how you define all those. Can you explain in detail. github.com/axnsan12/drf-yasg/tree/master/testprojBiotite
Sorry, I am not sure what you mean by "define all those". Do you mean my code example? Or the exampleproj in thier repository?Pola
I've added a thorough explanation about my code snippet, I hope it answers your question. gist.github.com/hayke102/28bddaaf357c49dc781579b3f7e829d5Pola
I have updated my answer to fix the issue you brought up and also support passing the parameter in json format to the endpoint: {"test_token": "TOKEN"}. Hope that solves all the issuesPola
I've again updated my answer, so you can use @api_view as you said. Take a look I really think its the best sulotion. Drf-yasg is much more popular than drf-spectacularPola
there are still some issues encountered gist.github.com/axilaris/bae7bb935fde0dc65804ce98e74a7cf1 (here is what I did, I made some fix to url-->path) and imgur.com/a/8DSeSYw (the swagger input is incorrect (should be input form and not json data, and there was naming _create). Could you try to provide guidance on this ? ThanksBiotite
Ok, i thought you wanted json. Ill upload a fixPola
This is for form data format: gist.github.com/hayke102/b392f9af89f9aaa63e9906ae5938b4acPola
About the name, try adding name="token_view" in url.pyPola
Im on my way home, ill send you the code when i get therePola
thank you very much. I'm also testing with drf_spectacular. The name underscore _create didnt exist at all. And there was no Django admin login. I tried to remove Django admin login for swagger yasg with this: #62032065, but then it complaints on "CSRF Failed: CSRF token missing or incorrect". Otherwise, your endpoint and input param works perfectly. by the way, path('token_view', token_view, name='token_view'), <<--- it didnt work with the name. It still have the underscore _createBiotite
To change the name that is displayed there you can supply operation_id="token_view_custom_message" in @swagger_auto_schema. I've updated the answer for examplePola
Let us continue this discussion in chat.Pola
thanks. that worked nicely on the naming. perfect. Right now, I want to just remove the admin login and make sure it does not have "CSRF Failed: CSRF token missing or incorrect". If that can be figured out, this is definitely the solution.Biotite
I got it working with this disabling - gist.github.com/axilaris/6269ee3898c7ad7465af820c5846de62. But somehow CSRF is gone. I think I need to find out how CSRF can be atleast enabled in the configurations.Biotite
So you do want csrf in your api?Pola
yes, in the original swagger api I had in Django 1.11, it has CSRF. But it does not require admin login. anyway there are reasons because providing admin login for this means too full access to the database, thats the reason I dont want to have the admin login enabled.Biotite
I think I got it with this: gist.github.com/axilaris/88a7ada22f8805cc383840072f9c5961. I'll do a bit more testing to confirm all. I think all looks good. Do advise if you think something is amiss. Thanks a whole lot!Biotite
@Biotite , If this answer helps to find your solution then accept this as Correct answer and Give bounty to Yuval.Contamination
yes definitely - the bounty goes to Yuval. Im just doing some validations. Will update in due time.Biotite
U
1

If you are just wanting something to test the API, Django rest framework actually comes with it's own browsable API. If you set a serializer_class on your APIView then the BrowsableAPIRenderer will figure out all of the relevant details for you.

The following should do the trick:

from rest_framework import serializers, status
from rest_framework.response import Response
from rest_framework.generics import GenericAPIView

class MySerializer(serializers.Serializer):
    token = serializers.CharField()

class MyView(GenericAPIView):

    serializer_class = MySerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        data = serializer.data

        return Response("test_token success", status=status.HTTP_200_OK)
# urls.py
urlpatterns = [
    ...
    path("api/test_token", views.MyView.as_view(), name="test_token")
]

(Notice that in Django 2+ we use path instead of the old url pattern. If you still want to use regex patterns you can use path_re).

The above assumes that you haven't changed the default settings for renderers. The default is:

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ]
}

Just browse to the relevant end-point and you'll have a nice interface for testing.

Under the hood it is is the fact that the serializer_class is set that enables this. This is the same way that DRF will auto generate a schema for use with something like swagger or redoc.

Untrue answered 30/1, 2021 at 12:23 Comment(2)
is there something that needs to be done on url.py to define the endpoints ?Biotite
@Biotite Not too much, you just point your end-point in url.py towards the view same as before (in this case a class-based view instead). Notice also since django 2.0 we use path() instead of url() to define our urls. It's a very similar syntax. If you want to use the old regex syntax you can use path_re.Untrue

© 2022 - 2024 — McMap. All rights reserved.