Valentin B.
Valentin B.

Reputation: 622

Convert GET parameters to POST data on a Request object in Django REST Framework

I am in the process of rewriting the backend of an internal website from PHP to Django (using REST framework).

Both versions (PHP and Django) need to be deployed concurrently for a while, and we have a set of software tools that interact with the legacy website through a simple AJAX API. All requests are done with the GET method.

My approach so far to make requests work on both sites was to make a simple adapter app, routed to 'http://<site-name>/ajax.php' to simulate the call to the Ajax controller. Said app contains one simple function based view which retrieves data from the incoming request to determine which corresponding Django view to call on the incoming request (basically what the Ajax controller does on the PHP version).

It does work, but I encountered a problem. One of my API actions was a simple entry creation in a DB table. So I defined my DRF viewset using some generic mixins:

class MyViewSet(MyGenericViewSet, CreateModelMixin):
    # ...

This adds a create action routed to POST requests on the page. Exactly what I need. Except my incoming requests are using GET method... I could write my own create action and make it accept GET requests, but in the long run, our tools will adapt to the Django API and the adapter app will no longer be needed so I would rather have "clean" view sets and models. It makes more sense to use POST for such an action.

In my adapter app view, I naively tried this:

request.method = "POST"
request.POST = request.GET

Before handing the request to the create view. As expected it did not work and I got a CSRF authentication failure message, although my adapter app view has a @csrf_exempt decorator...

I know I might be trying to fit triangle in squares here, but is there a way to make this work without rewriting my own create action ?

Upvotes: 4

Views: 7799

Answers (4)

John Moutafis
John Moutafis

Reputation: 23134

You can define a custom create method in your ViewSet, without overriding the original one, by utilizing the @action decorator that can accept GET requests and do the creation:

class MyViewSet(MyGenericViewSet, CreateModelMixin):
    ...
    @action(methods=['get'], detail=False, url_path='create-from-get')
    def create_from_get(self, request, *args, **kwargs):
        # Do your object creation here.

You will need a Router in your urls to connect the action automatically to your urls (A SimpleRouter will most likely do).
In your urls.py:

router = SimpleRouter()
router.register('something', MyViewSet, base_name='something')

urlpatterns = [
    ...
    path('my_api/', include(router.urls)),
    ...
]

Now you have an action that can create a model instance from a GET request (you need to add the logic that does that creation though) and you can access it with the following url:

your_domain/my_api/something/create-from-get

When you don't need this endpoint anymore, simply delete this part of the code and the action seizes to exist (or you can keep it for legacy reasons, that is up to you)!

Upvotes: 2

Valentin B.
Valentin B.

Reputation: 622

With the advice from all answers pointing to creating another view, this is what I ended up doing. Inside adapter/views.py:

from rest_framework.settings import api_settings
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.response import Response
from rest_framework import status

from mycoreapp.renderers import MyJSONRenderer
from myapp.views import MyViewSet

@api_view(http_method_names=["GET"])
@renderer_classes((MyJSONRenderer,))
def create_entity_from_get(request, *args, **kwargs):
    """This view exists for compatibility with the old API only. 
    Use 'POST' method directly to create a new entity."""
    query_params_copy = request.query_params.copy()
    # This is just some adjustments to make the legacy request params work with the serializer
    query_params_copy["foo"] = {"name": request.query_params.get("foo", None)}
    query_params_copy["bar"] = {"name": request.query_params.get("bar", None)}
    serializer = MyViewSet.serializer_class(data=query_params_copy)
    serializer.is_valid(raise_exception=True)
    serializer.save()
    try:
        headers = {'Location': str(serializer.data[api_settings.URL_FIELD_NAME])}
    except (TypeError, KeyError):
        headers = {}
    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

Of course I have obfuscated the names of everything specific to my project. Basically I reproduced almost exactly (except for a few tweaks to my query params) what happens in the create, perform_create and get_success_header methods of the DRF mixin CreateModelMixin in a single function based DRF view. Being just a standalone function it can sit in my adapter app views so that all legacy API code is sitting in one place only, which was my intent with this question.

Upvotes: 1

anjaneyulubatta505
anjaneyulubatta505

Reputation: 11665

As per REST architectural principles request method GET is only intended to retrieve the information. So, we should not perform a create operation with request method GET. To perform the create operation use request method POST.

Temporary Fix to your question

from rest_framework import generics, status

class CreateAPIView(generics.CreateView):

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.query_params)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(
            serializer.data,
            status=status.HTTP_201_CREATED,
            headers=headers)

    def get(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

Please refer below references for more information.
https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
https://learnbatta.com/blog/introduction-to-restful-apis-72/

Upvotes: 0

Aman Garg
Aman Garg

Reputation: 2547

You can write a method for your viewset (custom_get) which will be called when a GET call is made to your url, and call your create method from there.

class MyViewSet(MyGenericViewSet, CreateModelMixin):
    ...
    def custom_get(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

And in your urls.py, for your viewset, you can define that this method needs to be called on a GET call.

#urls.py
urlpatterns = [
    ...
    url(r'^your-url/$', MyViewSet.as_view({'get': 'custom_get'}), name='url-name'),
]

Upvotes: 0

Related Questions