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.
Related
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.
I have pagination enabled by default, which is based on PageNumberPagination; this had sufficed until now as the API was only used by a front-end. Now we try to build automation on top of it, and I’d like to pass the full, unpaginated result set back to the client.
Is there a way to disable pagination for specific requests, e.g. if a request parameter is passed?
I used a similer appraoch to accepted answer
class Unpaginatable(PageNumberPagination):
def paginate_queryset(self, queryset, request, view=None):
if request.query_params.get('get_all', False) == 'true':
return None
return super(BuildListPagination, self).paginate_queryset(queryset, request, view=view)
now if you pass ?get_all=true while making the request, you will get unpaginated response.
If you are using page number pagination style, the below solution may be better.
def paginate_queryset(self, queryset, request, view=None):
if 'page' not in request.query_params:
return None
return super().paginate_queryset(queryset, request, view)
So you simply send a request without the page query_param.
I actually did go with a custom pagination class:
class Unpaginatable(PageNumberPagination):
def paginate_queryset(self, queryset, request, view=None):
if getattr(request, 'get_all', False):
return None
return super(BuildListPagination, self).paginate_queryset(queryset, request, view=view)
Now I just have to set request.get_all = True in my viewset and I get all the items.
thnaks to this answer,
Else, if you use limit/offset you can use this in Generic List API View class:
def paginate_queryset(self, queryset):
if 'limit' not in self.request.query_params:
return None
return super().paginate_queryset(queryset)
It worked with python 3.9/ Django 4.0, In my case, this method had no argument named request and view, so I fixed it.
this will also won't render paginator json, when the response is not paginated.
You can approximate this with a request limit set to an unfeasibly large number, but you need to configure Django first:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
}
Then make a ridiculously high limit request:
GET https://api.example.org/accounts/?limit=999999999999
LimitOffsetPagination
- django-rest-framework.org
Actually DRF docs for this is very unclear, and there is no clear answer address this issue. After reading the source code, the followling code works for me.
from rest_framework.pagination import LimitOffsetPagination
class Unpaginated(LimitOffsetPagination):
def paginate_queryset(self, queryset, request, view=None):
self.count = self.get_count(queryset)
self.limit = self.get_limit(request)
self.offset = self.get_offset(request)
self.request = request
self.display_page_controls = False
return list(queryset)
class SomeViewSet(viewsets.ModelViewSet):
queryset = SomeModel.objects.all().order_by("-id")
pagination_class = Unpaginated
The key here is to override the paginate_queryset function in base class and return the whole queryset.
However, overriding this function is not documented at all in the docs or I just missed it.
https://www.django-rest-framework.org/api-guide/pagination/
https://github.com/encode/django-rest-framework/blob/master/rest_framework/pagination.py
I solve this problem in the following way:
make your own pagination class and inherit it from PageNumberPagination
redefine or add your code (with super()) in get_page_size function.
For me works this case:
def get_page_size(self, request):
super().get_page_size(request)
.......
self.page_size = self.max_page_size
return self.page_size
If I have a class view that looks like this,
class MovieDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
how do I make the serialize accept partial updates? currently where it stands Put will erase an existing data for said object.
If you are using the DRF route, use PATCH method instead of PUT.
if you write the urls configuration by yourself,
dispatch it to partial_update method in your RetrieveUpdateDestroyAPIView view.
If you get the serialize by yourself,
pass the partial=True to your Serializer
partial = kwargs.pop('partial', False)
serializer = self.get_serializer(instance, data=request.data, partial=partial)
Or you can just overwrite the get_serializer() method as:
def get_serializer(self, *args, **kwargs):
kwargs['partial'] = True
return super(MovieDetail, self).get_serializer(*args, **kwargs)
It is especially useful when the front-end guy use the ngResource of the AngularJS to call your API, which only supports the 'put' instead of the 'patch' by default.
Hope it helps.
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.
I'd like to write an except clause that redirects the user if there isn't something in a queryset. Any suggestions welcome. I'm a Python noob, which I get is the issue here.
Here is my current code:
def get_queryset(self):
try:
var = Model.objects.filter(user=self.request.user, done=False)
except:
pass
return var
I want to do something like this:
def get_queryset(self):
try:
var = Model.objects.filter(user=self.request.user, done=False)
except:
redirect('add_view')
return var
A try except block in the get_queryset method isn't really appropriate. Firstly, Model.objects.filter() won't raise an exception if the queryset is empty - it just returns an empty queryset. Secondly, the get_queryset method is meant to return a queryset, not an HttpResponse, so if you try to redirect inside that method, you'll run into problems.
I think you might find it easier to write a function based view. A first attempt might look like this:
from django.shortcuts import render
def my_view(request):
"""
Display all the objects belonging to the user
that are not done, or redirect if there are not any,
"""
objects = Model.objects.filter(user=self.request.user, done=False)
if not objects:
return HttpResponseRedirect("/empty-queryset-url/")
return render(request, 'myapp/template.html', {"objects": objects})
The advantage is that the flow of your function is pretty straight forward. This doesn't have as many features as the ListView generic class based view (it's missing pagination for example), but it is pretty clear to anyone reading your code what the view is doing.
If you really want to use the class based view, you have to dig into the CBV documentation for multiple object mixins and the source code, and find a suitable method to override.
In this case, you'll find that the ListView behaviour is quite different to what you want, because it never redirects. It displays an empty page by default, or a 404 page if you set allow_empty = False. I think you would have to override the get method to look something like this (untested).
class MyView(ListView):
def get_queryset(self):
return Model.objects.filter(user=self.request.user, done=False)
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
if len(self.object_list == 0):
return HttpResponseRedirect("/empty-queryset-url/")
context = self.get_context_data(object_list=self.object_list)
return self.render_to_response(context)
This is purely supplemental to #Alasdair's answer. It should really be a comment, but couldn't be formatted properly that way. Instead of actually redefining get on the ListView, you could override simply with:
class MyView(ListView):
allow_empty = False # Causes 404 to be raised if queryset is empty
def get(self, request, *args, **kwargs):
try:
return super(MyView, self).get(request, *args, **kwargs)
except Http404:
return HttpResponseRedirect("/empty-queryset-url/")
That way, you're not responsible for the entire implementation of get. If Django changes it in the future, you're still good to go.