Reputation: 622
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
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
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
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
.
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
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