Django Rest Framework 3.1 breaks pagination.PaginationSerializer - python

I just updated to Django Rest Framework 3.1 and it seems that all hell broke loose.
in my serializers.py I was having the following code:
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = task
exclude = ('key', ...)
class PaginatedTaskSerializer(pagination.PaginationSerializer):
class Meta:
object_serializer_class = TaskSerializer
which was working just fine. Now with the release of 3.1 I can't find examples on how to do the same thing since PaginationSerializer is no longer there.
I have tried to subclass PageNumberPagination and use its default paginate_queryset and get_paginated_response methods but I can no longer get their results serialized.
In other words my problem is that I can no longer do this:
class Meta:
object_serializer_class = TaskSerializer
Any ideas?
Thanks in advance

I think I figured it out (for the most part at least):
What we should have used from the very beginning is this:
Just use the built-in paginator and change your views.py to this:
from rest_framework.pagination import PageNumberPagination
class CourseListView(AuthView):
def get(self, request, format=None):
"""
Returns a JSON response with a listing of course objects
"""
courses = Course.objects.order_by('name').all()
paginator = PageNumberPagination()
# From the docs:
# The paginate_queryset method is passed the initial queryset
# and should return an iterable object that contains only the
# data in the requested page.
result_page = paginator.paginate_queryset(courses, request)
# Now we just have to serialize the data just like you suggested.
serializer = CourseSerializer(result_page, many=True)
# From the docs:
# The get_paginated_response method is passed the serialized page
# data and should return a Response instance.
return paginator.get_paginated_response(serializer.data)
For the desired page size just set the PAGE_SIZE in settings.py:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 15
}
You should be all set now with all the options present in the body of the response (count, next and back links) ordered just like before the update.
However there is one more thing that still troubles me: We should also be able to get the new html pagination controls which for some reason are missing for now...
I could definitely use a couple more suggestions on this...

I am not sure if this is the completely correct way to do it, but it works for my needs. It uses the Django Paginator and a custom serializer.
Here is my View Class that retrieves the objects for serialization
class CourseListView(AuthView):
def get(self, request, format=None):
"""
Returns a JSON response with a listing of course objects
"""
courses = Course.objects.order_by('name').all()
serializer = PaginatedCourseSerializer(courses, request, 25)
return Response(serializer.data)
Here is the hacked together Serializer that uses my Course serializer.
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
class PaginatedCourseSerializer():
def __init__(self, courses, request, num):
paginator = Paginator(courses, num)
page = request.QUERY_PARAMS.get('page')
try:
courses = paginator.page(page)
except PageNotAnInteger:
courses = paginator.page(1)
except EmptyPage:
courses = paginator.page(paginator.num_pages)
count = paginator.count
previous = None if not courses.has_previous() else courses.previous_page_number()
next = None if not courses.has_next() else courses.next_page_number()
serializer = CourseSerializer(courses, many=True)
self.data = {'count':count,'previous':previous,
'next':next,'courses':serializer.data}
This gives me a result that is similar to the behavior that the old paginator gave.
{
"previous": 1,
"next": 3,
"courses": [...],
"count": 384
}
I hope this helps. I still think there has got to be a beter way to do this wiht the new API, but it's just not documented well. If I figure anything more out, I'll edit my post.
EDIT
I think I have found a better, more elegant way to do it bey creating my own custom paginator to get behavior like I used to get with the old Paginated Serializer class.
This is a custom paginator class. I overloaded the response and next page methods to get the result I want (i.e. ?page=2 instead of the full url).
from rest_framework.response import Response
from rest_framework.utils.urls import replace_query_param
class CustomCoursePaginator(pagination.PageNumberPagination):
def get_paginated_response(self, data):
return Response({'count': self.page.paginator.count,
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'courses': data})
def get_next_link(self):
if not self.page.has_next():
return None
page_number = self.page.next_page_number()
return replace_query_param('', self.page_query_param, page_number)
def get_previous_link(self):
if not self.page.has_previous():
return None
page_number = self.page.previous_page_number()
return replace_query_param('', self.page_query_param, page_number)
Then my course view is very similar to how you implemented it, only this time using the Custom paginator.
class CourseListView(AuthView):
def get(self, request, format=None):
"""
Returns a JSON response with a listing of course objects
"""
courses = Course.objects.order_by('name').all()
paginator = CustomCoursePaginator()
result_page = paginator.paginate_queryset(courses, request)
serializer = CourseSerializer(result_page, many=True)
return paginator.get_paginated_response(serializer.data)
Now I get the result that I'm looking for.
{
"count": 384,
"next": "?page=3",
"previous": "?page=1",
"courses": []
}
I am still not certain about how this works for the Browsable API (I don't user this feature of drf). I think you can also create your own custom class for this. I hope this helps!

I realize over a year has passed since this was posted but hoping this helps others. The response to my similar question was the solution for me. I am using DRF 3.2.3.
Django Rest Framework 3.2.3 pagination not working for generics.ListCreateAPIView
Seeing how it was implemented gave me the solution needed to get pagination + the controls in the visible API.
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py#L39

Related

Django_Filters and queryset

Hi guys I'm building a simple django website where the user can look for long play. The problem right now is that I can't figure out why the filter, built with the django_filters library, doesn't work.
If you type "Queen" in the search bar in the home you are redirected to the searches.html where are listed all the LPs by Queen (just 4). On the left there's a filter. If you type "kind" in the form and submit then you should just see the album "Kind of Magic". This doesn't work. And I think the problem is the queryset "sp":
class searchesView(TemplateView):
template_name = "search/searches.html"
def post(self, request, *args, **kwargs):
print('FORM POSTED WITH {}'.format(request.POST['srh']))
srch = request.POST.get('srh')
if srch:
sp = Info.objects.filter(Q(band__icontains=srch)) | Info.objects.filter(Q(disco__icontains=srch))
myFilter = InfoFilter(request.GET, queryset=sp)
sp = myFilter.qs
paginator = Paginator(sp, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(self.request, 'search/searches.html', {'sp':sp, 'myFilter':myFilter,
'page_obj': page_obj
})
else:
return render(self.request, 'search/searches.html')
I think the problem is the queryset because then I tried to build a result.html page where I listed all the LPs in the database and applied the same filter above and here it works perfectly:
def result(request):
result = Info.objects.all()
myFilter = InfoFilter(request.GET, queryset=result)
result = myFilter.qs
return render (request, 'search/result.html', {'result':result, 'myFilter':myFilter})
The difference is that in the first case I've got a TemplateView with a certain queryset, and in the second case I've got a simple function with a different queryset. Someone know what the problem could be? I uploaded to code on repl if you want to try out the website: https://Django-Template.edwardradical.repl.co
Thanks a lot!
Issue has to do with the mix of POST/GET here.
You are reading parameters out of both post/get dictionaries here, which is not a great pattern to go down.
In this case, a POST request to the url with the filters should yield the correct results ie:
POST /searches/?band=kind&disco=
{
srh: data
}
In current solution, I believe the srh is not included in the post body, which causes issue in queryset.

Django - Correct method of consuming my own REST API internally in the views.py?

I created a Django REST API using serializers, viewsets and routers. My end points looks something like this:
http://www.website.com/api/items
http://www.website.com/api/items/available
serializer.py (omitting imports)
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = '__all__'
viewsets.py (omitting imports)
class ItemViewSet(viewsets.ModelViewSet):
queryset = Item.objects.all()
serializer_class = ItemSerializer
#action(methods=['GET'], detail=False)
def most_expensive(self, request):
query = self.get_queryset().order_by('price').last()
serialized = self.serializer_class(query)
return Response(serialized.data)
Now I want to able to access this API from my views.py to render the HTML with the available items:
This is the way im doing it right now:
views.py (omitting imports)
class ProductListView(View):
template = 'store/product_list.html'
def get(self, request):
items = requests.get('http://127.0.0.1:8000/api/items/available')
context = {'items': items}
return render(request, self.template, context=context)
Using the requests modules I have a couple of concerns, after measuring I noticed there is a 0.015 second delay for that request to go through and if I ever change the API endpoint I would have to adjust it here since its hard coded.
I can get my items using:
Item.objects.filter(available=True)
Which gives me the result pretty much instantly but I'm writing all the queries twice (once in my API and once in my views.py)
Is there a better way of doing this like calling the viewset class directly and getting the data from there?
Many thanks!
Calling the API endpoint in the same app is not considered a good practise.
An option would be to call your viewset method directly, like in https://stackoverflow.com/a/51149806/290036
The other one that I recommend is to use the same codebase for your API and for the view.
def get_avaialble_items():
items = Item.objects.filter(available=True)
...
return items
# Use get_avaialble_items both in ItemViewSet and ProductListView

Django Rest Framework partial update

I'm trying to implement partial_update with Django Rest Framework but I need some clarification because I'm stuck.
Why do we need to specify partial=True?
In my understanding, we could easily update Demo object inside of partial_update method. What is the purpose of this?
What is inside of serialized variable?
What is inside of serialized variable in partial_update method? Is that a Demo object? What function is called behind the scenes?
How would one finish the implementation here?
Viewset
class DemoViewSet(viewsets.ModelViewSet):
serializer_class = DemoSerializer
def partial_update(self, request, pk=None):
serialized = DemoSerializer(request.user, data=request.data, partial=True)
return Response(status=status.HTTP_202_ACCEPTED)
Serializer
class DemoSerializer(serializers.ModelSerializer):
class Meta:
model = Demo
fields = '__all__'
def update(self, instance, validated_data):
print 'this - here'
demo = Demo.objects.get(pk=instance.id)
Demo.objects.filter(pk=instance.id)\
.update(**validated_data)
return demo
I when digging into the source code of rest_framework and got the following findings:
For question 1. Why do we need to specify partial=True?
This question is related to HTTP verbs.
PUT: The PUT method replaces all current representations of the target resource with the request payload.
PATCH: The PATCH method is used to apply partial modifications to a resource.
Generally speaking, partial is used to check whether the fields in the model is needed to do field validation when client submitting data to the view.
For example, we have a Book model like this, pls note both of the name and author_name fields are mandatory (not null & not blank).
class Book(models.Model):
name = models.CharField('name of the book', max_length=100)
author_name = models.CharField('the name of the author', max_length=50)
# Create a new instance for testing
Book.objects.create(name='Python in a nut shell', author_name='Alex Martelli')
For some scenarios, we may only need to update part of the fields in the model, e.g., we only need to update name field in the Book. So for this case, client will only submit the name field with new value to the view. The data submit from the client may look like this:
{"pk": 1, name: "PYTHON IN A NUT SHELL"}
But you may have notice that our model definition does not allow author_name to be blank. So we have to use partial_update instead of update. So the rest framework will not perform field validation check for the fields which is missing in the request data.
For testing purpose, you can create two views for both update and partial_update, and you will get more understanding what I just said.
Example:
views.py
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import UpdateModelMixin
from rest_framework.viewsets import ModelViewSet
from rest_framework import serializers
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
class BookUpdateView(GenericAPIView, UpdateModelMixin):
'''
Book update API, need to submit both `name` and `author_name` fields
At the same time, or django will prevent to do update for field missing
'''
queryset = Book.objects.all()
serializer_class = BookSerializer
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
class BookPartialUpdateView(GenericAPIView, UpdateModelMixin):
'''
You just need to provide the field which is to be modified.
'''
queryset = Book.objects.all()
serializer_class = BookSerializer
def put(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
urls.py
urlpatterns = patterns('',
url(r'^book/update/(?P<pk>\d+)/$', BookUpdateView.as_view(), name='book_update'),
url(r'^book/update-partial/(?P<pk>\d+)/$', BookPartialUpdateView.as_view(), name='book_partial_update'),
)
Data to submit
{"pk": 1, name: "PYTHON IN A NUT SHELL"}
When you submit the above json to the /book/update/1/, you will got the following error with HTTP_STATUS_CODE=400:
{
"author_name": [
"This field is required."
]
}
But when you submit the above json to /book/update-partial/1/, you will got HTTP_STATUS_CODE=200 with following response,
{
"id": 1,
"name": "PYTHON IN A NUT SHELL",
"author_name": "Alex Martelli"
}
For question 2. What is inside of serialized variable?
serialized is a object wrapping the model instance as a serialisable object. and you can use this serialized to generate a plain JSON string with serialized.data .
For question 3. How would one finish the implementation here?
I think you can answer yourself when you have read the answer above, and you should have known when to use update and when to used partial_update.
If you still have any question, feel free to ask. I just read part of the source code of the rest framework, and may have not understand very deeply for some terms, and please point it out when it is wrong...
For partial update - PATCH http method
For full update - PUT http method
When doing an update with DRF, you are supposed to send request data that includes values for all (required) fields. This is at least the case when the request is via the PUT http method. From what I understand, you want to update one or at least not all model instance fields. In this case make a request with the PATCH http method. Django rest framework (DRF) will take care of it out of the box.
Example (with token auth):
curl -i -X PATCH -d '{"name":"my favorite banana"}' -H "Content-Type: application/json" -H 'Authorization: Token <some token>' http://localhost:8000/bananas/
So simple, just override init method of your serializer like that:
def __init__(self, *args, **kwargs):
kwargs['partial'] = True
super(DemoSerializer, self).__init__(*args, **kwargs)
Just a quick note as it seems that nobody has already pointed this out:
serialized = DemoSerializer(request.user, data=request.data, partial=True)
The first argument of DemoSerializer should be a Demo instance, not a user (at least if you use DRF 3.6.2 like me).
I don't know what you are trying to do, but this is a working example:
def partial_update(self, request, *args, **kwargs):
response_with_updated_instance = super(DemoViewSet, self).partial_update(request, *args, **kwargs)
Demo.objects.my_func(request.user, self.get_object())
return response_with_updated_instance
I do the partial update and then I do other things calling my_func and passing the current user and the demo instance already updated.
Hope this helps.
I had an issue where my multi-attribute/field validation in a rest_framework serializer was working with a POST /resources/ request but failing with a PATCH /resources/ request. It failed in the PATCH case because it was only looking for values in the supplied attrs dict and not falling back to values in self.instance. Adding a method get_attr_or_default to do that fallback seems to have worked:
class EmailSerializer(serializers.ModelSerializer):
def get_attr_or_default(self, attr, attrs, default=''):
"""Return the value of key ``attr`` in the dict ``attrs``; if that is
not present, return the value of the attribute ``attr`` in
``self.instance``; otherwise return ``default``.
"""
return attrs.get(attr, getattr(self.instance, attr, ''))
def validate(self, attrs):
"""Ensure that either a) there is a body or b) there is a valid template
reference and template context.
"""
existing_body = self.get_attr_or_default('body', attrs).strip()
if existing_body:
return attrs
template = self.get_attr_or_default('template', attrs)
templatecontext = self.get_attr_or_default('templatecontext', attrs)
if template and templatecontext:
try:
render_template(template.data, templatecontext)
return attrs
except TemplateRendererException as err:
raise serializers.ValidationError(str(err))
raise serializers.ValidationError(NO_BODY_OR_TEMPLATE_ERROR_MSG)
I don't know why, but for me, the only way to solve it was to override the validate method in the Serializer class.
Maybe it's related to the fact that I'm using MongoDB with Djongo
class DemoSerializer(serializers.ModelSerializer):
def validate(self, attrs):
self._kwargs["partial"] = True
return super().validate(attrs)
You forgot serializer.save()
You can finish it the following way . . .
class DemoViewSet(viewsets.ModelViewSet):
serializer_class = DemoSerializer
def partial_update(self, request, pk=None):
serializer = DemoSerializer(request.user, data=request.data, partial=True)
serializer.save()
serializer.is_valid(raise_exception=True)
return Response(serializer.data)
Also, you shouldn't need to override the update method in the serializer.

Django REST Custom View Parameters in Viewset with CoreAPI

If I define a django-rest-framework view framework as:
class ProblemViewSet(viewsets.ModelViewSet):
queryset = Problem.objects.all()
serializer_class = ProblemSerializer
#detail_route(methods=['post'], permission_classes=(permissions.IsAuthenticated,))
def submit(self, request, *args, **kwargs):
# my code here
return Response(...)
When I look at the CoreAPI schema that is defined, I find this:
problems: {
create(title, text, value, category, hint)
destroy(pk)
list([page])
partial_update(pk, [title], [text], [value], [category], [hint])
retrieve(pk)
submit(pk)
update(pk, title, text, value, category, hint)
}
I'd like the submit API endpoint to take an additional parameter, called answer, but so far I haven't figured out how to add such a custom parameter. I know I can just pass in the POST array, but that seems inelegant and un-RESTful. Any ideas?
I don't understand your question, but I think that you want to add in the input the filed answer to django-rest form or raw, you can do this adding in the serializer ProblemSerializer the next:
from rest_framework import serializers
...
class CustomerSerializer(serializers.ModelSerializer):
answer = serializers.SerializerMethodField()
class Meta:
model = Problem
fields = ['answer', 'my-normal-params',...]
def get_answer(self, problem):
if hasattr(problem, 'answer'):
request = self.context['request']
return answer
In get_answer method you will display in the json the value that you want, or you can return None (null) if you want,
If that is not your problem, please say me, I'll help you.

django serializers to json - custom json output format

I am quite new to django and recently I have a requirement of a JSON output, for which I use the following django code:
data = serializers.serialize("json", Mymodel.objects.all())
It works great, except that I get a output of:
[{"pk": 8970859016715811, "model": "myapp.mymodel", "fields": {"reviews": "3.5", "title": .....}}]
However, I would like the output to be simply either:
[{"reviews": "3.5", "title": .....}]
or,
[{"id": "8970859016715811", "reviews": "3.5", "title": .....}]
I was wondering if someone could point me to the right direction as to how to achieve this.
You can add 'fields' parameter to the serialize-function, like this:
data = serializers.serialize('xml', SomeModel.objects.all(), fields=('name','size'))
See: https://docs.djangoproject.com/en/dev/topics/serialization/
EDIT 1:
You can customize the serializer to get only the fields you specify.
From Override Django Object Serializer to get rid of specified model:
from django.core.serializers.python import Serializer
class MySerialiser(Serializer):
def end_object( self, obj ):
self._current['id'] = obj._get_pk_val()
self.objects.append( self._current )
# views.py
serializer = MySerialiser()
data = serializer.serialize(some_qs)
You'll need to write a custom Json serializer. Something like this should do the trick:
class FlatJsonSerializer(Serializer):
def get_dump_object(self, obj):
data = self._current
if not self.selected_fields or 'id' in self.selected_fields:
data['id'] = obj.id
return data
def end_object(self, obj):
if not self.first:
self.stream.write(', ')
json.dump(self.get_dump_object(obj), self.stream,
cls=DjangoJSONEncoder)
self._current = None
def start_serialization(self):
self.stream.write("[")
def end_serialization(self):
self.stream.write("]")
def getvalue(self):
return super(Serializer, self).getvalue()
The you can use it like this:
s = FlatJsonSerializer()
s.serialize(MyModel.objects.all())
Or you could register the serializer with django.core.serializers.register_serializer and then use the familiar serializers.serialize shortcut.
Take a look at the django implementation as a reference if you need further customization: https://github.com/django/django/blob/master/django/core/serializers/json.py#L21-62
I just came across this as I was having the same problem. I also solved this with a custom serializer, tried the "EDIT 1" method but it didn't work too well as it stripped away all the goodies that the django JSON encoder already did (decimal, date serialization), which you can rewrite it yourself but why bother. I think a much less intrusive way is to inherit the JSON serializer directly like this.
from django.core.serializers.json import Serializer
from django.utils.encoding import smart_text
class MyModelSerializer(Serializer):
def get_dump_object(self, obj):
self._current['id'] = smart_text(obj._get_pk_val(), strings_only=True)
return self._current
Sso the main culprit that writes the fields and model thing is at the parent level python serializer and this way, you also automatically get the fields filtering that's already built into django's JSON serializer. Call it like this
serializer = MyModelSerializer()
data = serializer.serialize(<queryset>, <optional>fields=('field1', 'field2'))
import json
_all_data = Reporter.objects. all()
json_data = json.dumps([{'name': reporter.full_name} for reporter in _all_data])
return HttpResponse(json_data, content_type='application/json')
Here Reporter is your Model

Categories

Resources