DRF, remove primary key from routed extra action - django

I have a view with some extra actions:
class MonthsViewSet(ModelViewSet):
authentication_classes = (TokenAuthentication,)
def get_queryset(self):
query_set = Month.objects.filter(user=self.request.user)
return query_set
serializer_class = MonthSerializer
#swagger_auto_schema(
manual_parameters=[AUTH_HEADER_PARAM, MonthParameters.DATE, MonthParameters.DAYS, MonthParameters.FARM])
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
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)
#action(detail=True)
def get_next_year(self, *args, **kwargs):
"""
Return the next 12 months.
"""
first_month, last_month = get_12_months(last=False)
query_set = self.get_queryset().filter(date__range=(first_month, last_month))
serializer = MonthSerializer(query_set, many=True)
return Response(serializer.data, status.HTTP_200_OK)
#action(detail=True)
def get_last_year(self, *args, **kwargs):
"""
Return the last 12 months available.
"""
first_month, last_month = get_12_months(last=True)
print(first_month, last_month)
query_set = self.get_queryset().filter(date__range=(first_month, last_month))
serializer = MonthSerializer(query_set, many=True)
return Response(serializer.data, status.HTTP_200_OK)
And I'm using the default router in my url:
months_router = DefaultRouter()
months_router.register('months', MonthsViewSet, 'months')
urlpatterns = [
path('', include(months_router.urls)),
]
So currently this is my URL:
/months/{date}/get_last_year/
the date is the primary key in my model.
Is there any way to change the action decorator settings to NOT use the primary key?
so the URL would become:
/months/get_last_year/

From the DRF doc,
Like regular actions, extra actions may be intended for either a single object, or an entire collection. To indicate this, set the detail argument to True or False. The router will configure its URL patterns accordingly.
set detail=False in your decorator.
#action(detail=False)
def get_last_year(self, *args, **kwargs):
# res of your code

Related

Django Rest Framework: DRYer pagination for custom actions

Suppose I need to set up several GET endpoints that look like this objects/past, objects/future. Example:
#action(detail=False, methods=["GET"], name="Past Objects")
def past(self, request, *args, **kwargs):
startdate = datetime.datetime.now()
some_user = UserProfile.objects.get(user__username="someuser")
queryset = self.queryset.filter(
other__attribute__profile=some_user,
creation_date__lte=startdate
).order_by("-creation_date")
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)
The above works just fine. But is there anyway to avoid the page = ... -> serializer= ... part?
I have specified this in my ModelViewSet:
pagination_class = CustomObjectPagination
But it seems the pagination is only auto-applied to default methods like get_queryset and not custom actions. Do I have to write this boilerplate every time I specify a custom action like past?
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)
Edit: Should have made it clearer that I'm asking specifically whether there's a built-in way to do the above.
I don't think we have any built-in to apply pagination on actions. But, we can have a simple decorator to do this. Make sure that your action returns a list or QuerySet when using this decorator.
from functools import wraps
from django.db.models import QuerySet
def paginate(func):
#wraps(func)
def inner(self, *args, **kwargs):
queryset = func(self, *args, **kwargs)
assert isinstance(queryset, (list, QuerySet)), "apply_pagination expects a List or a 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)
return inner
#paginate
#action(detail=False, methods=["GET"], name="Past Objects")
def past(self, request, *args, **kwargs):
startdate = datetime.datetime.now()
some_user = UserProfile.objects.get(user__username="someuser")
queryset = self.queryset.filter(
other__attribute__profile=some_user,
creation_date__lte=startdate
).order_by("-creation_date")
return queryset
I wrote a simple function for that:
def response_with_paginator(viewset, queryset):
page = viewset.paginate_queryset(queryset)
if page is not None:
serializer = viewset.get_serializer(page, many=True)
return viewset.get_paginated_response(serializer.data)
return Response(viewset.get_serializer(queryset, many=True).data)
Usage is like:
#action(...)
def comments(self, ...):
queryset = Comment.objects.filter(...)
return response_with_paginator(self, queryset)

Django rest-framework with GenericViewSet: Filter results base on query parameters from the url

My application is using GenericViewSet with ListModelMixin. I have used filter_backends and filter_class to filter out results. (see 'list': serializers.BookingListSerializer from screenshot below)
I am working on the following brief:
Let's say I have a list of animals which are pre-filtered (using filter_backends) and then shown on UI to the user.
Users can further filter results based on some search criteria from UI (let's say name, type, color). These filterations are handled by filter_class.
In a separate Tab on UI which only shows animals of type Dogs rather than the entire collection of animals. And which can again be filtered further based on the name & color.
I must create 2 separate end-points to show both kinds of results to the user (to have more control over results...ya screw DRY!). But I can't figure out how to create them in Django as both animals and dogs use the same django modal and the filter backends and filter class are applied only to the actual modal ie. on the list of animals.
I need simple def list1(request) and def list2(request) where I can filter the query_set based on request params and my filter backends and filter classes.
api.py
class BookingViewSet(
MultipleSerializerMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet
):
lookup_field = 'uuid'
queryset = models.Booking.objects.all()
permission_classes = [DRYPermissions, ]
filter_backends = [filters.BookingFilterBackend, DjangoFilterBackend, ]
filter_class = filters.BookingFilter
pagination_class = BookingViewSetPagination
serializer_class = serializers.BookingDetailSerializer
serializer_classes = {
'create': serializers.BookingCreateUpdateSerializer,
'update': serializers.BookingCreateUpdateSerializer,
'duplicate': serializers.BookingCreateUpdateSerializer,
'list': serializers.BookingListSerializer,
'list_drafts': serializers.BookingListSerializer,
'create_draft': serializers.BookingCreateUpdateSerializer,
'submit_draft': serializers.BookingCreateUpdateSerializer,
}
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
booking = services.create_booking(serializer.validated_data)
data = serializers.BookingDetailSerializer(booking, context={'request': request}).data
return response.Created(data)
def update(self, request, *args, **kwargs):
booking = self.get_object()
partial = kwargs.pop('partial', False)
serializer = self.get_serializer(booking, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
booking = services.update_booking(booking, serializer.validated_data)
async('shootsta.bookings.tasks.booking_update_google_calendar_event', booking.pk)
data = serializers.BookingDetailSerializer(booking, context={'request': request}).data
return response.Ok(data)
#detail_route(methods=['POST'], url_path='duplicate')
def duplicate(self, request, *args, **kwargs):
booking = self.get_object()
new_booking = services.duplicate_booking(booking)
data = serializers.BookingDetailSerializer(new_booking, context={'request': request}).data
return response.Created(data)
#list_route(methods=['GET'], url_path='list-drafts')
def list_drafts(self, request, *args, **kwargs):
# Code goes here! Here i'll get some params from url like state and title and then return filtered the results.
pass
#list_route(methods=['POST'], url_path='create-draft')
def create_draft(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
booking = services.create_booking(serializer.validated_data, constants.BookingMode.draft)
data = serializers.BookingDetailSerializer(booking, context={'request': request}).data
return response.Created(data)
#detail_route(methods=['POST'], url_path='submit-draft')
def submit_draft(self, request, *args, **kwargs):
booking = self.get_object()
booking.submit_draft(by=request.user)
booking.save()
data = serializers.BookingDetailSerializer(booking, context={'request': request}).data
return response.Ok(data)
#detail_route(methods=['POST'], url_path='approve')
def approve(self, request, *args, **kwargs):
booking = self.get_object()
booking.approve(by=request.user)
booking.save()
data = serializers.BookingDetailSerializer(booking, context={'request': request}).data
return response.Ok(data)
filters.py
# Standard Library
import operator
from functools import reduce
# Third Party
from django.db.models import Q
from django_filters import rest_framework as filters
from dry_rest_permissions.generics import DRYPermissionFiltersBase
# Project Local
from . import models
class BookingFilterBackend(DRYPermissionFiltersBase):
def filter_list_queryset(self, request, queryset, view):
if request.user.is_role_admin:
return queryset
if request.user.is_role_client:
return queryset.filter(Q(client=request.user.client))
if request.user.is_role_camop:
return queryset.filter(Q(camera_operator=request.user))
return queryset.filter(Q(created_by=request.user))
def filter_booking_title(queryset, name, value):
"""
Split the filter value into separate search terms and construct a set of queries from this. The set of queries
includes an icontains lookup for the lookup fields for each of the search terms. The set of queries is then joined
with the OR operator.
"""
lookups = ['title__icontains', ]
or_queries = []
search_terms = value.split()
for search_term in search_terms:
or_queries += [Q(**{lookup: search_term}) for lookup in lookups]
return queryset.filter(reduce(operator.or_, or_queries))
class BookingFilter(filters.FilterSet):
title = filters.CharFilter(method=filter_booking_title)
class Meta:
model = models.Booking
fields = [
'title',
'state',
'client',
]
class SampleViewset(.....):
#list_route(methods=['GET'])
def list_2(self, request, *args, **kwargs):
myqueryset = MyModel.objects.all() # or whatever queryset you need to serialize
queryset = self.filter_queryset(myqueryset)
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)
The key points you should notice here are,
1. The filtering process are being excecuted inside the self.filter_queryset() method, which return a QuerySet after filter applied.
2. You could use self.get_queryset() method in place of myqueryset = MyModel.objects.all() staement, which is the DRF Way of doing such things
UPDATE-1
If you want to use the default queryset , you could use the get_queryset() method as,
class SampleViewset(.....):
#list_route(methods=['GET'])
def list_2(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)
or simply,
class SampleViewset(.....):
#list_route(methods=['GET'])
def list_2(self, request, *args, **kwargs):
return self.list(self, request, *args, **kwargs)
I didn't quite get the question but I think you should do the same thing on your custom action that DRF does on its generic list. just call filter_queryset on your initial query for example:
class your_view(....):
...
...
def get_queryset2(self):
return YourotherModel.objects.all() ### or any thing your i.e. specific fiter on your general model
#action(methods=['GET'], detail=False)
def list2(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset2()) ### call filter_queryset on your custom query
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)

Django rest framework update Put not allowed

I'm trying to update an object of my database (but only one of the field), the problem is that when I try to make the update i get an error that says that the PUT method is not allowed.
Here's my View:
class DeviceViewSet(viewsets.ModelViewSet):
"""
Show, create and filter devices.
"""
queryset = Device.objects.all()
serializer_class = DeviceSerializer
def list(self, request, *args, **kwargs):
devices = Device.objects.filter(user=request.user.pk, role='E')
serializer = DeviceSerializer(devices, many=True)
return Response(serializer.data)
def create(self, request, *args, **kwargs):
data = {
'registration_id': request.data['regId'], 'user': request.user.pk, 'device_id': request.data['imei'],
'type': 'android', 'label': request.data['label'], 'role': request.data['role']
}
serializer = DeviceSerializer(data=data)
if serializer.is_valid():
serializer.save()
device = Device.objects.filter(device_id=request.data['imei'])
device.send_message("Enhorabuena!", "El dispositivo se ha registrado correctamente.")
return Response(serializer.data)
return Response(serializer.errors)
def update(self, request, *args, **kwargs):
device = Device.objects.filter(device_id=request.data['imei'])
device.registration_id = request.data['regId']
device.save()
serializer = DeviceSerializer(device)
return Response({'ok': 'oks'})
My serializer:
class DeviceSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), required=False)
class Meta:
model = Device
fields = '__all__'
My url:
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from decaught import views
urlpatterns = [
url(r'^devices/$', views.DeviceViewSet),
]
urlpatterns = format_suffix_patterns(urlpatterns)
I'm using Postman to send a PUT Request:
Any idea what is wrong?
When PUTting the resource identifier should be in the URL (pk). PUT request is idempotent.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT
In the DRF documentation, pk is passed as an argument to the update method
def update(self, request, pk=None):
pass
http://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions
Instead of passing it as a key:value pair and accessing it through request.data PUT call should be like
localhost:8000/devices/<PK-HERE>/
(sorry for not so good english)
The error is in our URL. You need to select some device to PUT information. Try with localhost:8000/devices/1/. I'm assuming that your API take objects by pk

"get_paginated_response" in django-rest-framework 3.0

In DRF 3.1 I can do this to get paginated response but it seems like the get_paginated_response() is not available in 3.0. What would be the equivalent?
class NoteList(ListCreateAPIView):
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
paged_queryset = self.paginate_queryset(queryset)
serializer = NoteSerializer(paged_queryset, many=True)
return self.get_paginated_response(serializer.data)
It's PaginationSerializer, set the serializer class by:
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'YourCustomPaginationSerializer'
or
class MyView(generics.GenericAPIView):
pagination_serializer_class = YourCustomPaginationSerializerClass
mixin.py
def list(self, request, *args, **kwargs):
instance = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(instance)
if page is not None:
serializer = self.get_pagination_serializer(page)
else:
serializer = self.get_serializer(instance, many=True)
return Response(serializer.data)
generics.py
def get_pagination_serializer(self, page):
"""
Return a serializer instance to use with paginated data.
"""
class SerializerClass(self.pagination_serializer_class):
class Meta:
object_serializer_class = self.get_serializer_class()
pagination_serializer_class = SerializerClass
context = self.get_serializer_context()
return pagination_serializer_class(instance=page, context=context)
If you are doing the migration stuffs, don't forget the pagination params in settings.py or View, has been changed to new paginator.
such as. PAGINATE_BY_PARAM -> page_size_query_param.
anyhow, I think the new paginator is much confortable than the previous one.
see more detials on Pagination Docs

How do I create multiple model instances with Django Rest Framework?

I would like to save and update multiple instances using the Django Rest Framework with one API call. For example, let's say I have a "Classroom" model that can have multiple "Teachers". If I wanted to create multiple teachers and later update all of their classroom numbers how would I do that? Do I have to make an API call for each teacher?
I know currently we can't save nested models, but I would like to know if we can save it at the teacher level.
Thanks!
I know this was asked a while ago now but I found it whilst trying to figure this out myself.
It turns out if you pass many=True when instantiating the serializer class for a model, it can then accept multiple objects.
This is mentioned here in the django rest framework docs
For my case, my view looked like this:
class ThingViewSet(viewsets.ModelViewSet):
"""This view provides list, detail, create, retrieve, update
and destroy actions for Things."""
model = Thing
serializer_class = ThingSerializer
I didn't really want to go writing a load of boilerplate just to have direct control over the instantiation of the serializer and pass many=True, so in my serializer class I override the __init__ instead:
class ThingSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
many = kwargs.pop('many', True)
super(ThingSerializer, self).__init__(many=many, *args, **kwargs)
class Meta:
model = Thing
fields = ('loads', 'of', 'fields', )
Posting data to the list URL for this view in the format:
[
{'loads':'foo','of':'bar','fields':'buzz'},
{'loads':'fizz','of':'bazz','fields':'errrrm'}
]
Created two resources with those details. Which was nice.
I came to a similar conclusion as Daniel Albarral, but here's a more succinct solution:
class CreateListModelMixin(object):
def get_serializer(self, *args, **kwargs):
""" if an array is passed, set serializer to many """
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
Here's another solution, you don't need to override your serializers __init__ method. Just override your view's (ModelViewSet) 'create' method. Notice many=isinstance(request.data,list). Here many=True when you send an array of objects to create, and False when you send just the one. This way, you can save both an item and a list!
from rest_framework import status, viewsets
from rest_framework.response import Response
class ThingViewSet(viewsets.ModelViewSet):
"""This view snippet provides both list and item create functionality."""
#I took the liberty to change the model to queryset
queryset = Thing.objects.all()
serializer_class = ThingSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, many=isinstance(request.data,list))
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)
I couldn't quite figure out getting the request.DATA to convert from a dictionary to an array - which was a limit on my ability to Tom Manterfield's solution to work. Here is my solution:
class ThingSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
many = kwargs.pop('many', True)
super(ThingSerializer, self).__init__(many=many, *args, **kwargs)
class Meta:
model = Thing
fields = ('loads', 'of', 'fields', )
class ThingViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet ):
queryset = myModels\
.Thing\
.objects\
.all()
serializer_class = ThingSerializer
def create(self, request, *args, **kwargs):
self.user = request.user
listOfThings = request.DATA['things']
serializer = self.get_serializer(data=listOfThings, files=request.FILES, many=True)
if serializer.is_valid():
serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
And then I run the equivalent of this on the client:
var things = {
"things":[
{'loads':'foo','of':'bar','fields':'buzz'},
{'loads':'fizz','of':'bazz','fields':'errrrm'}]
}
thingClientResource.post(things)
I think the best approach to respect the proposed architecture of the framework will be to create a mixin like this:
class CreateListModelMixin(object):
def create(self, request, *args, **kwargs):
"""
Create a list of model instances if a list is provided or a
single model instance otherwise.
"""
data = request.data
if isinstance(data, list):
serializer = self.get_serializer(data=request.data, many=True)
else:
serializer = self.get_serializer(data=request.data)
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)
Then you can override the CreateModelMixin of ModelViewSet like this:
class <MyModel>ViewSet(CreateListModelMixin, viewsets.ModelViewSet):
...
...
Now in the client you can work like this:
var things = [
{'loads':'foo','of':'bar','fields':'buzz'},
{'loads':'fizz','of':'bazz','fields':'errrrm'}
]
thingClientResource.post(things)
or
var thing = {
'loads':'foo','of':'bar','fields':'buzz'
}
thingClientResource.post(thing)
EDIT:
As Roger Collins suggests in his response is more clever to overwrite the get_serializer method than the 'create'.
You can simply overwrite the get_serializer method in your APIView and pass many=True into get_serializer of the base view like so:
class SomeAPIView(CreateAPIView):
queryset = SomeModel.objects.all()
serializer_class = SomeSerializer
def get_serializer(self, instance=None, data=None, many=False, partial=False):
return super(SomeAPIView, self).get_serializer(instance=instance, data=data, many=True, partial=partial)
I came up with simple example in post
Serializers.py
from rest_framework import serializers
from movie.models import Movie
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = [
'popularity',
'director',
'genre',
'imdb_score',
'name',
]
Views.py
from rest_framework.response import Response
from rest_framework import generics
from .serializers import MovieSerializer
from movie.models import Movie
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
class MovieList(generics.ListCreateAPIView):
queryset = Movie.objects.all().order_by('-id')[:10]
serializer_class = MovieSerializer
permission_classes = (IsAuthenticated,)
def list(self, request):
queryset = self.get_queryset()
serializer = MovieSerializer(queryset, many=True)
return Response(serializer.data)
def post(self, request, format=None):
data = request.data
if isinstance(data, list): # <- is the main logic
serializer = self.get_serializer(data=request.data, many=True)
else:
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
These line are the actual logic of Multiple Instance -
data = request.data
if isinstance(data, list): # <- is the main logic
serializer = self.get_serializer(data=request.data, many=True)
else:
serializer = self.get_serializer(data=request.data)
If you are confused with many=True, see this
When we send data it will be inside list somewhat like this -
[
{
"popularity": 84.0,
"director": "Stanley Kubrick",
"genre": [
1,
6,
10
],
"imdb_score": 8.4,
"name": "2001 : A Space Odyssey"
},
{
"popularity": 84.0,
"director": "Stanley Kubrick",
"genre": [
1,
6,
10
],
"imdb_score": 8.4,
"name": "2001 : A Space Odyssey"
}
]
The Generic Views page in Django REST Framework's documentation states that the ListCreateAPIView generic view is "used for read-write endpoints to represent a collection of model instances".
That's where I would start looking (and I'm going to actually, since we'll need this functionality in our project soon as well).
Note also that the examples on the Generic Views page happen to use ListCreateAPIView.
Most straightforward method I've come across:
def post(self, request, *args, **kwargs):
serializer = ThatSerializer(data=request.data, many=isinstance(request.data, list))
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)