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
I have two separate methods:
to load and validate a csv file FileUploadView(APIView) [PUT]
to add new objects to the database based on their uploaded file data
CsvToDatabase [POST]
For this purpose, 2 different url addresses are used
Now I want to combine this functionality into one, so that the file is loaded with processing and creation of instances in the database is done on a single request. That is, the final goal - the application user sends the file to the server and then everything happens automatically.
file upload
class FileUploadView(APIView):
parser_classes = (MultiPartParser, FormParser)
permission_classes = (permissions.AllowAny,)
def put(self, request, format=None):
if 'file' not in request.data:
raise ParseError("Empty content")
f = request.data['file']
filename = f.name
if filename.endswith('.csv'):
file = default_storage.save(filename, f)
r = csv_file_parser(file)
status = 204
print(json.dumps(r))
else:
status = 406
r = "File format error"
return Response(r, status=status)
create instances
class CsvToDatabase(APIView):
permission_classes = (permissions.AllowAny,)
serializer_class = VendorsCsvSerializer
def post(self, request, format=None):
r_data = request.data
...
#some logic
...
serializer = VendorsCsvSerializer(data=data)
try:
serializer.is_valid(raise_exception=True)
serializer.save()
except ValidationError:
return Response({"errors": (serializer.errors,)},
status=status.HTTP_400_BAD_REQUEST)
else:
return Response(request.data, status=status.HTTP_200_OK)
how can I correctly combine two methods in one endpoint so that if csv file validation is successful, the POST method will be called? Or maybe it's better to leave two different urls and send the json received after parsing the .csv file to the url with the POST method? This option seems to me easier to test. but how do I do it?
Thanks!
I solved it this way
class FileUploadView(APIView):
parser_classes = (MultiPartParser, FormParser)
# renderer_classes = [JSONRenderer]
permission_classes = (permissions.AllowAny,)
serializer_class = VendorsCsvSerializer
def put(self, request, format=None):
if 'file' not in request.data:
raise ParseError("Empty content")
f = request.data['file']
filename = f.name
if filename.endswith('.csv'):
file = default_storage.save(filename, f)
r = csv_file_parser(file)
status = 204
response = Response(r)
self.post(request=response)
else:
status = 406
r = "File format error"
return Response(r, status=status)
def post(self, request, format=None):
r_data = request.data
....
serializer = VendorsCsvSerializer(data=data)
try:
serializer.is_valid(raise_exception=True)
serializer.save()
except ValidationError:
return Response({"errors": (serializer.errors,)},
status=status.HTTP_400_BAD_REQUEST)
else:
return Response(request.data, status=status.HTTP_200_OK)
I hope this is the right way
We have an APIView (FooView) that can be accessed directly through a URL.
We have another APIView APIKeyImportView that will reuse FooView depending of file name (it is done this way for compatibility with an API).
However when request.FILES is accessed from APIKeyImportView to look at the file names, request.FILES becomes empty in FooView.
It appears that accessing request.FILES will makes it un-useable by the nested view.
Is there a way around this?
class FooView(APIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, vendor):
file = request.FILES.get('file')
if not file:
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response()
class APIKeyImportView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (ApiKeyAuthentication,)
def post(self, request):
file = request.FILES.get('file')
if not file:
return Response(status=status.HTTP_400_BAD_REQUEST)
name = file.name
if name.startswith('FOO'):
return FooView.as_view()(request=request)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
Removing the validation on request.Files in APIKeyImportView will make it accessible in FooView but it kinds of miss the point.
Inspecting request in PyCharm will also make it un-useable in FooView since the debugger will call the properties.
class APIKeyImportView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (ApiKeyAuthentication,)
def post(self, request):
return FooView.as_view()(request=request)
These solutions are not working:
django modifying the request object
Tested on the following versions:
Django 1.9.5
django-rest-framework 3.3.3
Python 3.4.2
A workaround I found was to pass request.FILES but I am not sure if it has side effects
class FooView(APIView):
permission_classes = (permissions.IsAuthenticated,)
_files = None
#property
def request_files(self):
if self._files:
return self._files
return self.request.FILES
def post(self, request, vendor):
file = self.request_files.get('file')
if not file:
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response()
class APIKeyImportView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (ApiKeyAuthentication,)
def post(self, request):
file = request.FILES.get('file')
if not file:
return Response(status=status.HTTP_400_BAD_REQUEST)
name = file.name
if name.startswith('FOO'):
# Passing FILES here
return FooView.as_view(_files=request.FILES)(request=request)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
I'm sure there must be something I don't understand about type hierarchies and initialisation in python...
I wanted to log post bodies with django rest framework like suggested here on stackoverflow: by overriding initial and finalize_response.
This is how my mixin looks like:
class LoggingMixin(object):
"""
Provides full logging of requests and responses
"""
def finalize_response(self, request, response, *args, **kwargs):
# do the logging
if settings.DEBUG:
logger.debug("[{0}] {1}".format(self.__class__.__name__, response.data))
return super(LoggingMixin, self).finalize_response(request, response, *args, **kwargs)
def initial(self, request, *args, **kwargs):
# do the logging
if settings.DEBUG:
try:
data = request._data
logger.debug("[{0}] {1}".format(self.__class__.__name__, data))
except exceptions.ParseError:
data = '[Invalid data in request]'
super(LoggingMixin, self).initial(self, request, *args, **kwargs)
And my view:
class BulkScan(LoggingMixin, generics.ListCreateAPIView):
"""
Provides get (list all) and post (single) for scans.
"""
queryset = Scan.objects.all()
serializer_class = ScanSerializer
authentication_classes = (OAuth2Authentication,)
permission_classes = (IsAuthenticated,)
# insert the user on save
def pre_save(self, obj):
for scan in obj:
scan.user = self.request.user
def post(self, request, *args, **kwargs):
serializer = ScanSerializer(data=request.DATA, many=True)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
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)
Yet, a post or get request fails, complaining that the request.user property is not present. This must automagically be injected there. If I don't overwrite initial then everything is fine and the user is set when APIView.initial is called.
For now, I resorted to overriding get and post and then logging the content of the post body but I don't get why the user property is not set when I override the method.
Many thanks for any clarification on this matter.
You're calling the super implementation of initial wrong. Don't pass self:
super(LoggingMixin, self).initial(request, *args, **kwargs)
Hopefully that fixes it — there doesn't seem to be anything else wrong.
#carlton-gibson is correct, but there's one other thing. I had this same issue. Fixed it by calling the super()'s initial() before doing the logging.
def initial(self, request, *args, **kwargs):
# do the logging
result = super(LoggingMixin, self).initial(request, *args, **kwargs)
if settings.DEBUG:
try:
data = request._data
logger.debug("[{0}] {1}".format(self.__class__.__name__, data))
except exceptions.ParseError:
data = '[Invalid data in request]'
return result
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)