I wish to implement my new API with a nested resource.
Example: /api/users/:user_id/posts/
Will evaluate to all of the posts for a specific user. I haven't seen an working example for this use case, maybe this isn't the right way for implementing rest API?
As commented by Danilo, the #link decorator got removed in favor of #list_route and #detail_route decorators.
Update: #detail_route & #list_route got deprecated in favor of #action.
Here's the alternate solutions:
Solution 1:
#detail_route()
def posts(self, request, pk=None):
owner = self.get_object()
posts = Post.objects.filter(owner=owner)
context = {
'request': request
}
post_serializer = PostSerializer(posts, many=True, context=context)
return Response(post_serializer.data)
Solution 2:
Try drf-nested-routers. Haven't tried this out yet, but looks promising, many are already using it. Looks like an advanced version of what we are already trying to achieve.
Hope this helps.
To map /api/users/:user_id/posts/ you can decorate a posts method inside your ViewSet with #link()
from rest_framework.decorators import link
from rest_framework.response import Response
class UserViewSet(viewsets.ModelViewSet):
model = User
serializer_class = UserSerializer
# Your regular ModelViewSet things here
# Add a decorated method like this
#link()
def posts(self, request, pk):
# pk is the user_id in your example
posts = Post.objects.filter(owner=pk)
# Or, you can also do a related objects query, something like:
# user = self.get_object(pk)
# posts = user.post_set.all()
# Then just serialize and return it!
serializer = PostSerializer(posts)
return Response(serializer.data)
As commented by Danilo Cabello earlier you would use #detail_route or #list_route instead of #link(). Please read the documentation for "Routers", section "Extra link and actions" and "ViewSets", section "Marking extra actions for routing" for detailed explanations.
Related
It is necessary to display the configured 10 records per swagger page (book api).
The BookListView class in views.py looks like this:
class BookListView(APIView):
def get(self, request):
author = set([item.get('ID') for item in Author.objects.values('ID').all()])
saler = set([item.get('name') for item in Saler.objects.values('name').all()])
if author:
salers = Saler.objects.filter(name__in=list(saler - author))
else:
salers = Saler.objects.all()
serializer = SalerListSerializer(salers, many = True)
return Response(serializer.data)
Now all records are displayed at once, I would like to add pangination and display 10 records on one page.
I added 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.DESIRED_PAGINATION_STYLE', 'PAGE_SIZE': 100 to the settings.py file, but since I am using APIView, this does not work.
What is the way to implement the idea?
You need to call your paginator's paginate_queryset method. Since you're using a APIView, it does not have many of the built-in functions to do this for you, but the process is as follows:
Instantiate a paginator:
from rest_framework.settings import api_settings
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
paginator = pagination_class()
Get a page from your queryset and serialize it:
page = paginator.paginate_queryset(queryset, request, view=self)
serializer = self.get_serializer(page, many=True)
Return a paginated response:
return paginator.get_paginated_response(serializer.data)
This is how django-rest does it with its ListAPIView class. You can go though the code easily by checking out the ListAPIView's list method here.
However, I suggest using a ListAPIView instead of an APIView since it already handles
pagination for you.
I am trying to make an api backend of something like reddit. I want to ensure that whoever is creating a post (model Post) within a particular subreddit is a member of that subreddit (subreddit model is Sub). Here is my latest effort, which works but seems pretty sloppy, and the serializer for some context.
Post permissions.py
class IsMemberOfSubOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
elif request.data:
# prevent creation unless user is member of the sub
post_sub_pk = get_pk_from_link(request.data['sub'])
user = request.user
user_sub_pks = [sub.pk for sub in user.subs.all()]
if not (post_sub_pk in user_sub_pks):
return False
return True
Post serializers.py
from .models import Post
from redditors.models import User
from subs.models import Sub
class PostSerializer(serializers.HyperlinkedModelSerializer):
poster = serializers.HyperlinkedRelatedField(
view_name='user-detail',
#queryset=User.objects.all(),
read_only=True
)
sub = serializers.HyperlinkedRelatedField(
view_name='sub-detail',
queryset=Sub.objects.all()
)
class Meta:
model = Post
fields = ('url', 'id', 'created', 'updated', 'title', 'body',
'upvotes', 'sub', 'poster')
The issue with this approach is that since 'sub' is a hyperlinkedRelatedField on the Post serializer what I get back from request.data['sub'] is just the string hyperlink url. I then have a function, get_pk_from_link that uses regex to read the pk off the end of the url. Then I can use that to grab the actual model I want and check things. It would be nice if there were a more direct way to access the Sub model that is involved in the request.
I have tried searching the fields of the arguments that are available and I can't find a way to get to the Sub object directly. Is there a way to access the Sub model object through the hyperlink url?
I have also solved this problem by just using a serializer field validator (not shown above), but I am interested to know how to do it this way too. Maybe this is just a bad idea and if so please let me know why.
You are right, parsing the url is not the way to go. Since you want to perform the permission check before creating a Post object, I suspect you cannot use object level permissions either, because DRF does not call get_object in a CreateAPIView (since the object does not exist in the database yet).
Considering this is a "business logic" check, a simpler approach would be to not have that permission class at all and perform the check in your perform_create hook in your view (I had asked a similar question about this earlier):
from rest_framework.exceptions import PermissionDenied
# assuming you have a view class like this one for creating Post objects
class PostList(generics.CreateApiView):
# ... other view stuff
def perform_create(self, serializer):
sub = serializer.get('sub') # serializer is already validated so the sub object exists
if not self.request.user.subs.filter(pk=sub.pk).exists():
raise PermissionDenied(detail='Sorry, you are not a member of this sub.')
serializer.save()
This saves you hassle of having to perform that url parsing as the serializer should give you the Sub object directly.
Prologue:
I have seen this question arising in more than one posts:
Django Rest Framework - APIView Pagination
Pagination not working in DRF APIView
Django rest framework global pagination parameters not working for ModelViewSet
and can also be applied here:
Combine ListModelMixin with APIView to show pagination
I have composed an example on SO Documentation to unify my answers in the above questions but since the Documentation will get shutdown on August 8 2017, I will follow the suggestion of this widely upvoted and discussed meta answer and transform my example to a self-answered post.
Of course I would be more than happy to see any different approach as well!!
Question:
I want to use a Non Generic View/Viewset (eg: APIView) on a Django Rest Framework project.
As I read on the pagination documentation:
Pagination is only performed automatically if you're using the generic views or viewsets. If you're using a regular APIView, you'll need to call into the pagination API yourself to ensure you return a paginated response. See the source code for the mixins.ListModelMixin and generics.GenericAPIView classes for an example.
Can I still continue using a non generic view/viewset?
How can I implement pagination on it?
We can find a solution without the need to reinvent the wheel:
Let's have a look on how the generics pagination is implemented:
django-rest-framework/rest_framework/generics.py.
That is exactly what we are going to use to our view as well!
Let's assume that we have a global pagination setup like the following in:
settings.py:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS':
'rest_framework.pagination.DESIRED_PAGINATION_STYLE',
'PAGE_SIZE': 100
}
In order not to bloat our view/viewset's code, we can create a custom mixin to store our pagination code:
class MyPaginationMixin(object):
#property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
def paginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination
is disabled.
"""
if self.paginator is None:
return None
return self.paginator.paginate_queryset(
queryset, self.request, view=self)
def get_paginated_response(self, data):
"""
Return a paginated style `Response` object for the given
output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
Then on views.py:
from rest_framework.settings import api_settings
from rest_framework.views import APIView
from my_app.mixins import MyPaginationMixin
class MyView(APIView, MyPaginationMixin):
queryset = OurModel.objects.all()
serializer_class = OurModelSerializer
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
# We need to override the get method to insert pagination
def get(self, request):
...
page = self.paginate_queryset(self.queryset)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
And now we have an APIView with pagination.
Im kind of new to Django Rest Framework. I know it is possible to post data using the Browsable API, I just don't know how. I have this simple view:
class ProcessBill(APIView):
def post(self, request):
bill_data = request.data
print(bill_data)
return Response("just a test", status=status.HTTP_200_OK)
When I go to the url that points to this view, I get the rest_framework browsable api view with the response from the server method not allowed which is understandable cause I am not setting a def get() method. But ... how can I POST the data? I was expecting a form of some kind somewhere.
EDIT
This is a screenshot of how the browsable API looks for me, it is in spanish. The view is the same I wrote above but in spanish. As you can see ... no form for POST data :/ .
Since you are new I will recommend you to use Generic views, it will save you lot of time and make your life easier:
class ProcessBillListCreateApiView(generics.ListCreateAPIView):
model = ProcessBill
queryset = ProcessBill.objects.all()
serializer_class = ProcessBillSerializer
def create(self, request, *args, **kwargs):
bill_data = request.data
print(bill_data)
return bill_data
I will recommend to go also through DRF Tutorial to the different way to implement your endpoint and some advanced feature like Generic views, Permessions, etc.
Most likely the user has read-only permission. If this is the case make sure the user is properly authenticated or remove the configuration from the projects settings.py if it is not necessary as shown below.
```#'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
#],```
Read more on permissions here.
I have a ViewSet like
class CustomerViewSet(ModelViewSet):
queryset = models.Customer.objects.all()
serializer_class = serializers.CustomerSerializer
filter_class = filters.CustomerFilterSet
#detail_route
def licenses(self, request, pk=None):
customer = self.get_object()
licenses = Item.objects.active().sold().for_customer(customer)
serializer = serializers.ItemSerializer(licenses, many=True)
return Response(serializer.data)
Strapped into urls.py with router.register(r'customers', views.CustomerViewSet)
i can GET /api/customers and GET /api/customers/1000, but GET /api/customers/1000/licenses is not found. It gives me 404. The flow never enters the licenses method.
I looked at similar questions here, but they used an incorrect signature, which I do not: def licenses(self, request, pk=None)
python 3.4.0
djangorestframework==3.2.3
EDIT: Once again, I find my answer less than a minute after asking... Apparantly, the decorater needs parenthesees like #detail_route(). I thought these were always optional by convention..?
For a post request you'll need to specify the method because the detail_route decorator will route get requests by default.
Like so: #detail_route(methods=['POST'])
By default, router adds trailing slash to the url. So, GET /api/customers/ would be working instead of GET /api/customers. If you don't want to use trailing slash you can pass trailing_slash = False to router initializer.
E.g.-
router = DefaultRouter(trailing_slash = False)
router.register(r'customers', views.CustomerViewSet)
If that does not works, then there is a problem with the way you are importing router urls into main urls.