How to properly get a serializer instance in Django view? - python

I'm trying to get an instance of a serializer in my overwritten list method and then pass it in through perform_create. Basically what this code does is it checks if the queryset is empty and if it is, we do a perform_create. The problem is that I'm trying to get an instance of the serializer so I can pass it in to the perform_create method. I don't believe the line serializer = self.get_serializer(data=request.data)
correctly grabs the serializer as it shows nothing when I try to log it. Any help is appreciated, thanks.
class ExampleViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwner)
def list(self, request):
queryset = self.get_queryset()
name = self.request.query_params.get('name', None)
# print(request.data)
if name is not None:
queryset = queryset.filter(name=name)
if (queryset.count() == 0):
serializer = self.get_serializer(data=request.data)
print(serializer)
return self.perform_create(serializer)
return HttpResponse(serializers.serialize('json', queryset))
elif name is None:
return HttpResponse(serializers.serialize('json', queryset))

As far as I can see, with
serializer = self.get_serializer(data=request.data)
you are trying to access POST data while responding to a GET request.
DRF ViewSets offer the methods:
list (called upon an HTTP GET request)
create (called upon an HTTP POST request)
retrieve (called upon an HTTP GET request)
update (called upon an HTTP PUT request)
partial_update (called upon an HTTP PATCH request)
destroy (called upon an HTTP DELETE request)
Also see this explicit example binding HTTP verbs to ViewSet methods
So if
you are POSTing data, the list method isn't called at all (as suggested by #Ivan in the very first comment you got above).
The solution is to move the code to the appropriate method, i.e create
Otherwise
your client is GETting, the list method is called, but request.data will be empty at best.
The solution is to make the client provide the parameters for the creation as GET parameters, along with name.
That way the view will find them in self.request.query_params
In case you have a form, simply change the way it sends its data by making it use HTTP GET. See here for further info

Related

How ListCreateAPIView works?

I am just new to the Django Rest Framework and I want to clearly understand how ListCreateAPIView works.
We just can provide a queryset, serializer_class and it will create a read-write endpoint.
I was looking for info on the official doc but didn't find what I want.
Any information will be helpful for me.
ListCreateAPIView is a generic APIView that allows for GET (list) and POST (create) requests.
You can read the source code and maybe get a better understanding
Basically, ListCreateAPIView has the method get() which will call the method list() in mixins.ListModelMixin. The list method will instance the serializer, filter, paginate the queryset and return a response based on the queryset and serializer you have defined in your class.
If you want a deeper understanding I recommend you to read the source code, it can be confusing at first but when you starting using it you will understand it better.
the story begins from the get method so when calling get it will call list method
this is how list method looks like it will call queryset and make pagination then serialize the data to returned as response
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
for more information, you can visit this link
https://www.cdrf.co/3.12/rest_framework.generics/ListAPIView.html

DRF , route a retrieve function to a post function

I have a Django rest framework API that gets a POST request and has a retrive method in the view.
I want that when the user presses the post button it will route the URL to the render created in the retrieve method of the view class.
code:
#views.py
class LocationInfoViewSet(ModelViewSet):
# Order all objects by id, reversed.
queryset = LocationInfo.objects.all().order_by('-id')
serializer_class = LocationInfoSerializer
def retrieve(self, request, *args, **kwargs):
"""
This method is used to get the last object created by the user and render a map associated with the
mission's name.
"""
data = self.queryset[0]
serialized_data = LocationInfoSerializer(data, many=False)
points = list(serialized_data.data.values())
assign_gdt1 = GeoPoint(lat=points[2], long=points[3])
assign_gdt2 = GeoPoint(lat=points[4], long=points[5])
assign_uav = GeoPoint(lat=points[6], long=points[7], elevation=points[-3])
# Geo locations from the POST request.
gdt1 = [assign_gdt1.get_lat(), assign_gdt1.get_long()]
gdt2 = [assign_gdt2.get_lat(), assign_gdt2.get_long()]
uav = [assign_uav.get_lat(), assign_uav.get_long(), assign_uav.get_elevation()]
mission_name = points[1]
try:
# Check if a file already exists in the DB.
HTMLFileInteractionWithDB.table = THREE_POINTS_TRINAGULATION
openfile = HTMLFileInteractionWithDB.return_file_from_db(mission_name=mission_name)
return render(request, openfile)
except:
# Create a new file if one does not exists.
# The main function Creates an HTML File to be rendered.
return render(request, main(gdt1, gdt2, uav,
gdt1_elev=assign_gdt1.get_elevation(),
gdt2_elev=assign_gdt2.get_elevation(),
mission_name=mission_name
)
)
mission name is a primary key, So to access to the retrieve method the user need to go to the URL line and write the mission name for the method to work.
So, how and where in my project (urls,view...) do I create this route.
Exmpale:
I'm a little confused as to the purpose of this view.
The retrieve method is correctly used when it is retrieving a specific object from the queryset list using the pk. IE, one of your LocationInfo objects. It's always a get request.
Your retrieve method is also missing the pk parameter, which is sometimes defaulted as None.
retrieve(self, request, pk=None)
If I were you, I'd create a completely separate view or viewset method/action for this.
Instead of using retrieve for this, we can create a completely new method:
from rest_framework.decorators import action
class LocationInfoViewSet(ModelViewSet):
# Order all objects by id, reversed.
queryset = LocationInfo.objects.all().order_by('-id')
serializer_class = LocationInfoSerializer
# {The rest of your methods, etc...}
#action(detail=False, methods=["post"])
def last_object_by_user(self, request, *args, **kwargs):
# your query to get the last object by your user
queryset = LocationInfo.objects.filter(created_by=request.user).latest()
# The rest of your code
This will create a new endpoint called /{name}/last_object_by_user/ that you can make post requests to.
You'd also notice that I've modified your queryset a bit. Your current queryset gives us the last LocationInfo created by any user. Did you create a field in LocationInfo that tracks who created that LocationInfo?
Here's the documentation for this: Marking extra actions for routing
Note: I did not test this code so if you copy-paste this, it might not work, but the idea is what's important.

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 ?
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)!
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.
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'),
]
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/

Django rest framework - API view complains about NoneType response

I am using a ModelViewSet to create objects from parameters received in a POST request. The serialiser looks like this:
class FooSerializer(ModelSerializer):
class Meta:
model = Foo
fields = '__all__'
I want to intercept the request, and perform a check on it (against a method of the model, if it matters) before allowing the creation to continue. In vanilla django forms, I would override the form_valid method, do the check, and then call super().form_valid(...). I am trying to do the same here:
class BookingView(ModelViewSet):
queryset = DirectBooking.objects.all()
serializer_class = DirectBookingSerializer
def create(self, request):
print(request.data)
#Perform check here
super().create(request)
This works, in the sense that it creates an object in the DB, but the trace shows an error:
AssertionError: Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from the view, but received a `<class 'NoneType'>`
This seems strange, as I would expect the super().save to return the appropriate response.
I am aware that I will need to return a response myself if the check fails (probably a 400), but I would still like to understand why it is failing here.
A view should return a HttpResponse. In a ViewSet, you do not implement .get(..) and .post(..) directly, but these perform some processing, and redirect to other functions like .create(..), and .list(..).
These views thus should then return a HttpResponse (or "friends"), here you call the super().create(request), but you forget to return the response of this call as the result of your create(..) version.
You should thus add a return statement, like:
class BookingView(ModelViewSet):
queryset = DirectBooking.objects.all()
serializer_class = DirectBookingSerializer
def create(self, request):
print(request.data)
#Perform check here
return super().create(request)

Django REST Framework: accepting multiple POSTed objects with a list_route

I have a ModelViewSet with an extra list_route to handle GET/POST for a certain list of objects:
class PickViewset(viewsets.ModelViewSet):
queryset = Pick.objects.all()
serializer_class = PickSerializer
def get_queryset(self):
#gets the correct queryset
#list_route(methods=['get', 'post'])
def update_picks(self, request, league, week, format = None):
if request.method == 'POST':
#process/save objects here
else:
#otherwise return the requested list
Thanks to the answer on my earlier question, this action can successfully handle a GET request as well as POST- however, when I try to POST more than one object, I get a JSON error:
"detail": "JSON parse error - Extra data: line 90 column 6 - line 181 column 2 (char 3683 - 7375)"
Where the specified location corresponds to the end of the first object. How can I change update_picks to handle a list of objects as well? Also, if this request may be a mix of new and updated existing objects, should I even be using POST for all, or just handle each POST/PUT on a per-object basis?
I considered adding a CreateModelMixin to the Viewset, however it can already create- but just one object. The ListCreateAPIView seems to be similar- it doesn't have an inherent list creation, but rather just the CreateModelMixin and ListModelMixin- both of which I think are provided by default when using a ModelViewset.
I think you have to overwrite the post method (see the question here Django Rest Framework Batch Create) and parse the json on your own using JSONParser().parse()
def post(self, request, *args, **kwargs):
if request.DATA['batch']:
json = request.DATA['batchData']
stream = StringIO(json)
data = JSONParser().parse(stream)
request._data = data
return super(CharacterDatumList, self).post(request, *args, **kwargs)

Categories

Resources