Move POST parameters to query parameters before processing request - django

I need to move the request.POST parameters to the request.query_params QueryDict.
Is there an accepted way of doing this?
Background
I am using datatables, with a DRF backend, which is working fine. I am moving the application to integration and ... it stops working. Why? Request URL too big (on the 7000 characters range) - which was not a problem in my dev host ...
So, I am looking for a solution to that problem. The first solution is to use POST instead of GET. That works, but the library integrating DRF with datatables is not processing the form parameters of the POST request. Because of that, filtering, pagination and so on have stopped working.
The easiest thing to solve this would be to put the form parameters into the query parameters, and let the backend process the request as if it was a normal GET request.
This is what I am doing at the moment:
class DataViewSet(viewsets.ModelViewSet):
queryset = Data.objects.all()
serializer_class = DataSerializer
def create(self, request, *args, **kwargs):
# DataTable uses a lot of parameters which do not fit into a normal URL. To get the data we need to do POST,
# so that the parameters are sent in the body
# We hijack the create method to list the data
return self.list(request, *args, **kwargs)

I'm not aware of any accepted ways of doing this. But let me offer you an idea. It is probably on the opposite side of what accepted means.
The rest_framework.request.Request.query_params look like this:
#property
def query_params(self):
return self._request.GET
Im thinking about substituting the self._request.GET with self._request.POST
class DataViewSet(viewsets.ModelViewSet):
queryset = Data.objects.all()
serializer_class = DataSerializer
def create(self, request, *args, **kwargs):
# DataTable uses a lot of parameters which do not fit into a normal URL. To get the data we need to do POST,
# so that the parameters are sent in the body
# We hijack the create method to list the data
request._request.GET = request._request.POST
return self.list(request, *args, **kwargs)
This should work for POST data. Sending files to this endpoint is probably bad idea.
NOTE: This is very fishy and could introduce bugs in the future. Without look into your code i cannot predict the side effects.

Related

Use parameter as part of the url and how to handle it on django rest framework

Noob question here.
I have an endpoint configure in this way
router.register(r'company-to-audit', myapp_views.CompanyToAuditViewSet,
base_name='company-to-audit')
and on views.py
class CompanyToAuditViewSet(viewsets.ModelViewSet):
...
#list_route(methods=['get'], url_path=r'companies')
def get_companies(self, request, **kwargs):
# Lots of logic, return a json with Response
return Response(serializer.data, status=status.HTTP_200_OK)
Currently, to call this endpoint, I need 3 parameters. One of them is mandatory, City, and the other two are optional.
so, the calls look like this
http://localhost:8001/company-to-audit/companies/?city=Austin
or
http://localhost:8001/company-to-audit/companies/?city=Austin&codeID=3&regulation=oldest
How can I modify this so instead of using those URL, could use the following ones:
http://localhost:8001/company-to-audit/Austin
or
http://localhost:8001/company-to-audit/Austin/?codeID=3&regulation=oldest
So, basically, transform the mandatory City in part of the URL. And also, currently, I obtaining the parameters with request.query_params, for example
request.query_params.get('city')
If it is possible to transform City in part of the URL, how can I capture the value?
in #list_route, you can change url_path to companies/(?P<city>[^/.]+)
then in view you can do it in these ways:
def get_companies(self, request, **kwargs):
kwargs.get("city")
def get_companies(self, request, city="<default>", **kwargs):
city # access like a variable
def get_companies(self, request, **kwargs):
self.kwargs.get("city")

Django REST Framework: DRY way to specify viewmodels

I would like to have a clear way for declaring a viewmodel for my Django REST endpoints, which the incoming requests must adhere to and which would take care of the validation. I mean something like viewmodels in Spring or other Java projects.
The most primitive way is just to work with the request.data (or request.POST) dictionary-like object, but in this case all the validation is hardcoded to the view layer.
A little better approach would be to use serializers, but using them is still quite verbose and in the end you get back a dict, unless you implement the .create() method witch makes the code even more verbose. Here is my current approach:
class MyView(APIView):
def post(self, request, format=None):
serializer= MySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
what_i_actually_need = serializer.validated_data
...
return Response('Something')
Is there a more DRY way to do it?
Inspired by the Spring approach I implemented something similar. Let me first show the public API and then include the code behind it.
class GreetingViewModel(ViewModel):
first_name= serializers.CharField(max_length=100)
last_name= serializers.CharField(max_length=120)
class GreetingView(APIView):
def post(self, request, format=None):
# ViewModel will be validated here
viewmodel = GreetingViewModel.get_viewmodel(request.data)
return Response({'message': f'Good afternoon, '
'dear {viewmodel.first_name} {viewmodel .last_name}!'})
And here is the code behind it:
class ViewModel(serializers.Serializer):
"""
Used to strictly specify a viewmodel expected from the request body.
Call `get_viewmodel` to convert request body to a Python object after it is validated.
"""
#classmethod
def get_viewmodel(cls, data):
instance = cls(data=data)
instance.is_valid(raise_exception=True)
return instance.save()
def create(self, validated_data):
obj = type('ViewModel', (), validated_data)()
return obj
Note that this is just my personal idea, so I appreciate comments and suggestions.

Django rest framework bulk: Correct URL syntax for bulk operations?

I'm attempting to carry out bulk DELETEs/PUTs on my Django API, mymodel inherits from MultipleDBModelViewSet which in turn inherits from the BulkModelViewSet
The bulk destroy method is as follows
class MultipleDBModelViewSet(BulkModelViewSet):
...
...
def bulk_destroy(self, request, *args, **kwargs):
ids = json.loads(request.query_params.get("ids"))
if not ids:
return super().destroy(request, *args, pk=kwargs.pop("pk"), **kwargs)
else:
return super().bulk_destroy(request, *args, **kwargs)
In my urls.py file I define the URL used to interact with my model:
router.register(r"v1/mymodels", mymodels_views_v1.MyModelViewSet)
this allows me to GET, POST, PUT and DELETE on the URL (works perfectly at present):
www.my-api.com/v1/mymodels/{{mymodel_id}}
Can I use this same URL for bulk operations? If so, what is the correct syntax?
eg: www.my-api.com/v1/mymodels/?ids=[{{mymodel_id1}},{{mymodel_id2}}]
If not, what changes should I make?
Thanks
There are two things they are saying at their documentation.
Most API urls have two URL levels for each resource:
url(r'foo/', ...)
url(r'foo/(?P<pk>\d+)/', ...)
The second url however is not applicable for bulk operations because
the url directly maps to a single resource. Therefore all bulk
generic views only apply to the first url.
That means it won't take url kwarg parameter.
The only exception to this is bulk delete. Consider a DELETE request
to the first url. That can potentially delete all resources without
any special confirmation. To try to account for this, bulk delete
mixin allows to implement a hook to determine if the bulk delete
request should be allowed:
class FooView(BulkDestroyAPIView):
def allow_bulk_destroy(self, qs, filtered):
# custom logic here
# default checks if the qs was filtered
# qs comes from self.get_queryset()
# filtered comes from self.filter_queryset(qs)
return qs is not filtered
Solution:-
You could do like this
class SimpleViewSet(generics.BulkModelViewSet):
def filter_queryset(self, queryset):
ids = self.request.query_params.get('ids')
if ids:
return queryset.filter(id__in=ids.split(',')
# returns normal query set if no param
return queryset

Best way to process different POST requests in a single Class Based View?

Developing my first application here and im using a class based view(django.views.generic.base.View) to handle requests from a webpage.
On the webpage I have different forms that send out POST requests, for example, there is text posting form, comment form, vote button, etc. and Im checking POST.has_key() to see which form has been posted and processing according to that.
Whats a better way to do it? And is it possible to define method names such as post_text, post_comment etc and configure dispatch() to run the method accordingly?
I would do it like this:
class AwesomeView(View):
def post(self, request, *args, **kwargs):
# This code is basically the same as in dispatch
# only not overriding dispatch ensures the request method check stays in place.
# Implement something here that works out the name of the
# method to call, without the post_ prefix
# or returns a default method name when key is not found.
# For example: key = self.request.POST.get('form_name', 'invalid_request')
# In this example, I expect that value to be in the 'key' variable
handler = getattr(
self, # Lookup the function in this class
"post_{0}".format(key), # Method name
self.post_operation_not_supported # Error response method
)
return handler(request, *args, **kwargs)
def post_comment(self, request, *args, **kwargs):
return HttpResponse("OK") # Just an example response

Django rest framework migrating from 0.x to 2.1.9

After resolving some of my troubles while converting from django-rest-framwork 0.3.2 to the lates 2.1.9 I cannot see to fix this one (which i agree with a blog of Reinout.... it's a real pain in the ...)
I had this code:
class ApiSomeInputView(View):
form = ApiSomeForm
permissions = (IsAuthenticated, )
resource=SomeResource
def get(self, request):
"""
Handle GET requests.
"""
return "Error: No GET request Possible, use post"
def post(self, request, format=None):
some_thing = self.CONTENT['some_thing']
# check if something exist:
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise _404_SOMETHING_NOT_FOUND
#Note exludes are set in SomeResource
data = Serializer(depth=4).serialize(something)
return Response(status.HTTP_200_OK, data)
Now I have followed the tutorial and saw how you can do this different (maybe even prettier). By using slug in the url.
However.... I want to keep things backward compatible for the client side software... so I want to have this without putting the value of the query in the url. The client side uses json data and ContentType json in the header of a post.
In the first version of django rest framwork, I even got a nice browsable form in which to fill in the values for this query
My question: how to get this done in the latest version?
I can't seem to get a form in the views.... where I can fill in values and use in the proces
maybe good to post what I have tried until sofar...
first I changed the ModelResource in a Serializer:
class SomethingSerializer(HyperlinkedModelSerializer):
class Meta:
model = Something
#exclude = ('id',)
depth = 4
and than the view changed in to:
class ApiSomeInputView(APIView):
permissions = (IsAuthenticated, )
def post(self, request, format=None):
some_thing = request.DATA['some_thing']
# check if something exist: .... well actually this above already does not work
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise _404_SOMETHING_NOT_FOUND
serializer = SomethingSerializer(something)
return Response(status.HTTP_200_OK, serializer.data)
Note: Bases upon the accepted answer (by Tom Christie) I als put an answer in which I show how I got it working (in more detail).
When you're inheriting from APIView, the browseable API renderer has no way of knowing what serializer you want to use to present in the HTML, so it falls back to allowing you to post a plain JSON (or whatever) representation.
If you instead inherit from GenericAPIView, set the serializer using the serializer_class attribute, and get an instance of the serializer using the get_serializer(...) method - see here, then the browseable API will use a form to display the user input.
Based upon the answer of Tom Christie (which I'll accept as the answer). I got it working:
I made an extra serializer which defines the field(s) to be shown to fill in for the post and shown using the GenericAPIView... (correct me if I Am wrong Tom, just documenting it here for others... so better say it correct)
class SomethingSerializerForm(Serializer):
some_thing = serializers.IntegerField()
And with this serializer and the other one I aready had.
And a view:
class ApiSomeInputView(GenericAPIView):
permissions = (IsAuthenticated, )
model = Something
serializer_class = SomethingSerializerForm
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA)
if not serializer.is_valid():
raise ParseError(detail="No valid values")
some_thing = request.DATA['some_thing']
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise Http404
serializer = SomethingSerializer(something)
return Response(serializer.data)
Above is working, and exactly the same as before....
I still got the feeling I Am abusing the Serializer class as a Form.