Django REST framework post array of objects - django

I am using Django REST framework for API and Angular SPA with Restangular to communicate with the API. Sometimes, I have to add more than one object using the API and I think I can send them together in an array and do this in one request.
I receive wrong input error when I'm trying to add more than one object from the REST framework web interface. I am passing objects or array of objects like below:
// this { "text": "gdhg", },{ "text": "gdhg", },{ "text": "gdhg", }
// or this [{ "text": "gdhg", },{ "text": "gdhg", },{ "text": "gdhg", }]
But I receive ParseError. Where I am wrong and what do I have to change to fix this?

Another example that supports posting an array as well as posting a single object. Might be useful for anyone else looking for such an example.
class BookViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
"""
ViewSet create and list books
Usage single : POST
{
"name":"Killing Floor: A Jack Reacher Novel",
"author":"Lee Child"
}
Usage array : POST
[{
"name":"Mr. Mercedes: A Novel (The Bill Hodges Trilogy)",
"author":"Stephen King"
},{
"name":"Killing Floor: A Jack Reacher Novel",
"author":"Lee Child"
}]
"""
queryset = Book.objects.all()
serializer_class = BookSerializer
search_fields = ('name','author')
def create(self, request, *args, **kwargs):
"""
#checks if post request data is an array initializes serializer with many=True
else executes default CreateModelMixin.create function
"""
is_many = isinstance(request.data, list)
if not is_many:
return super(BookViewSet, self).create(request, *args, **kwargs)
else:
serializer = self.get_serializer(data=request.data, many=True)
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 am not sure if the problem still exist. But the solution suggested by fiver did not work for me.
What works for me is overriding the get_serializer method ONLY.
def get_serializer(self, instance=None, data=None,
files=None, many=True, partial=False):
return super(ViewName, self).get_serializer(instance, data, files,
many, partial)
If you will notice I am setting default many=True in arguments of get_serializer.
Apart from that nothing is required. Overridng of create method is also not required.
Also if you are defining the pre_save and post_save method in the views, expects the list(iterable) as the argument(as you are posting the list) of method not just a single object.
def post_save(self, objects, *args, **kwargs):
"""
In the post_save, list of obj has been created
"""
for obj in objects:
do_something_with(obj)

Here's an example for setting up bulk POSTing in a ListCreateAPIView using the Django REST Framework:
class SomethingList(generics.ListCreateAPIView):
model = Something
serializer_class = SomethingSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, 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)
The important part here is the many=True argument to the get_serializer() method. Then, to make Angular play nice with this, you can define a service factory as:
.factory('Something', ['$resource', function ($resource) {
return $resource(
"url_to_something",
{},
{
save: {
method: 'POST',
isArray: true
}
}
);
}])
Where the important part is the isArray: true. If you want to preserve posting single JSON objects, you could change save above to something like saveBulk or similar.

Building on vibhor's answer:
class ListableViewMixin(object):
def get_serializer(self, instance=None, data=None, many=False, *args, **kwargs):
return super(ListableViewMixin, self).get_serializer(
instance=instance, data=data, many=isinstance(instance, list) or isinstance(data, list),
*args, **kwargs)
Make your view inherit from this mixin class to automatically determine if a many=True serializer should be used.

If you want to post a list you have to pass in JSON encoded data.
headers = {"Token": "35754sr7cvd7ryh454"}
recipients = [{'name': 'Ut est sed sed ipsa',
'email': 'dogoka#mailinator.com',
'group': 'signers'},
{'name': 'Development Ltda.',
'email': 'test#test.com',
'group': 'signers'}
]
requests.post(url, json=recipients, headers=headers)
requests.post(url, json=recipients, headers=headers)
Use json keyword argument (not data) so the data is encoded to JSON and the Content-Type header is set to application/json.
By default, Django Rest Framework assumes you are passing it a single object. To serialize a queryset or list of objects instead of a single object instance, you should pass the many=True flag when instantiating the serializer. You can then pass a queryset or list of objects to be serialized.
To do it, you'll have to override the .create() method of your view:
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = self.get_serializer(data=request.data, many=many)
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)
Documentation: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-multiple-objects

Related

Django restframework can't get post data with a multipart parser

i am trying to save an object with an image using django restframework but when i use the FormParser and MultiPartParser classes the request.data object get seemingly encoded msgs and when i try to decode using utf-8 it outputs an error saying this data is not utf-8
i want to have access to the request.data data and be able to save the image for future requests
here is my view function:
#parser_classes([FormParser, MultiPartParser])
#api_view(['GET', 'POST'])
#permission_classes([IsAuthenticated])
def products(request):
if request.method == 'POST':
print(request.data)
serializer = ProductSerializer(data=request.data)
serializer.initial_data['user'] = request.user.pk
serializer.initial_data['price'] = float(
request.data['price'])
serializer.initial_data['quantity'] = int(
request.data['quantity'])
if serializer.is_valid():
serializer.save()
return Response({'message': "product added"}, status=status.HTTP_201_CREATED)
else:
print(serializer.errors)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
my front end code:
export const addProduct = async (product) => {
const fd = new FormData();
fd.append("name", product.name);
fd.append("price", product.price);
fd.append("quantity", product.quantity);
fd.append("image_url", product.image_url);
console.log(product.image_url) //this prints a file object
const response = await fetch(`${URL}/products`, {
method: "POST",
headers: {
"Content-Type": `multipart/form-data; boundary=${fd._boundary}`,
},
body: fd,
})
return response
}
The image data is not being encoded as UTF-8 (Maybe! and if so), you can use the FileField or ImageField field in your serializer, along with the FormParser and MultiPartParser classes!
[UPDATED] Your view should be:
#parser_classes([FormParser, MultiPartParser])
#api_view(['GET', 'POST'])
#permission_classes([IsAuthenticated])
def products(request):
if request.method == 'POST':
serializer = ProductSerializer(data=request.data)
serializer.initial_data['user'] = request.user.pk
if 'price' in request.data:
serializer.initial_data['price'] = float(request.data['price'])
else:
return Response({'error': 'price not found in request data'}, status=status.HTTP_400_BAD_REQUEST)
if 'quantity' in request.data:
serializer.initial_data['quantity'] = int(request.data['quantity'])
else:
return Response({'error': 'quantity not found in request data'}, status=status.HTTP_400_BAD_REQUEST)
if serializer.is_valid():
serializer.save()
return Response({'message': "product added"}, status=status.HTTP_201_CREATED)
else:
print(serializer.errors)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
And the serializer:
class ProductSerializer(serializers.ModelSerializer):
image_url = serializers.ImageField()
class Meta:
model = Product
fields = ['name', 'price', 'quantity', 'image_url', 'user']
so when i removed my headers it worked fine, so apparently if you choose to use multipart/form-data, the boundary must not appear in the file data that the server eventually receives.
The problem with multipart/form-data is that the boundary separator must not be present in the file data (see RFC 2388; section 5.2 also includes a rather lame excuse for not having a proper aggregate MIME type that avoids this problem).
so i fixed it by removing the boundary in the front end which makes my frontend code look something like this
const fd = new FormData();
fd.append("name", product.name);
fd.append("price", product.price);
fd.append("quantity", product.quantity);
fd.append("image_url", product.image_url);
console.log(product.image_url) // this print a File object
const response = await fetch(`${URL}/products`, {
method: "POST",
headers: {
Authorization: `token ${localStorage.getItem("auth")}`
},
body: fd,
})

string list as a parameter in url in django

i am working in a search view which take two inputs as a filter :
search word
2.cities (multi select)
i did it using serializer and it worked but paggination does not work because it was a post method , so i'm trying to take these inputs from url parameters , i tried this pattern :
path(r"search/<str:search>/(?P<city>\w*)/",
SearchView.as_view({"get": "search"}), name="search"),
but when i browse : http://127.0.0.1:8000/company/search/taxi/montrial/
it return Not Found: /company/search/
so how to pass the paramteres or is there another way to use paggination with post method
I suggest to use pagination with get request or in case you should do it with post
class CustomPagination(pagination.PageNumberPagination):
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(), #you can read page number from url and put it here
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'results': data
})
to read data from url you can use request.query_params
https://www.django-rest-framework.org/api-guide/pagination/
i solved it using the serializer method, inherit from viewsets.GenericViewsets which has pagingation methods
class SearchView(viewsets.GenericViewSet):
permission_classes = [IsDriver]
queryset = CompanyProfile.objects.all()
serializer_class = SearchSerializer
#action(methods=['post'], detail=False)
def search(self, request, format=None):
serializer = SearchSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
page = self.paginate_queryset(serializer.data["companies"])
if page is not None:
# used CompanyProfileSerializer to serialize the companies query
serializer = CompanyProfileSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(serializer.data)

Django-Rest: how to access data if JSON or URL-encoded inside view?

I need to access some nested information inside request.data inside a post definition. The data sent is in the following form:
{
...
"licence": {
"tenant_name": "tenant1",
...
}
}
Since I'm using Django Rest with the default parsers installed (JSONParser and FormParser) I could receive JSON or HTML form content inside the request. I'd like to keep both and don't change the default parser_classes of the view. The request.data has different types and representations based on the content:
HTML-encoded: <QueryDict: {..., 'licence.tenant_name': ['tenant1']}>
JSON: {..., 'licence': {'tenant_name': 'tenant1'}}
To handle this I currently check on the type. Is there a better way as a general use case?
class SubscribeView(views.APIView):
serializer_class = SubscriptionSerializer
permission_classes = (AllowAny, )
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
tenant_name = request.data['licence']['tenant_name'] if type(request.data) is dict else request.data['licence.tenant_name']
# perform actions on tenant_name
serializer.save()
status_code = status.HTTP_201_CREATED
response = {
'success': True,
'status_code': status_code,
'message': 'New subscription successfully created',
'subscription': serializer.data
}
return Response(response, status=status_code)
I didn't catch that I can just use the serializer itself! It has built in features to handle multiple data encodings.
So I can just transform this:
tenant_name = request.data['licence']['tenant_name'] if type(request.data) is dict else request.data['licence.tenant_name']
To this:
tenant_name = serializer.validated_data.get('licence', {}).get('tenant_name', '')

Django pass extra data to ModelSerializer create from a ModelViewSet serializer.save()

I have this ModelViewSet
def create(self, request, *args, **kwargs):
data_to_save = request.data
pharmacy = Pharmacy.objects.get(pk=request.data['pharmacy'])
serializer = self.get_serializer(data=data_to_save)
serializer.is_valid(raise_exception=True)
serializer.save(myArg=pharmacy)
headers = self.get_success_headers(serializer.data)
return Response({'results': serializer.data}, status=status.HTTP_201_CREATED, headers=headers)
The self.get_serializer(...) points to a class PharmacyUserSerializer(serializers.ModelSerializer): ...
The PharmacyUserSerializer(...), I'm overriding the create(...) function like so
def create(self, validated_data):
request = self.context['request']
myArg = self.context['myArg']
pharmacy = request.user.pharmacy
user = User.objects.create_user(
**validated_data,
user_type=c.PHARMACY,
pharmacy=pharmacy
)
return user
ACcording to the DRF docs, this line looks right (passing arguments to the save method)
serializer.save(myArg=pharmacy)
Doing the above gives the error,
TypeError: 'myArg' is an invalid keyword argument for this function
So what's going on? What's the right way to pass data (i guess I'm missing something in the docs).
And how do I intercept this extra data in the PharmacyUserSerializer
You can only pass attribute of your model to serializer in save method not whatever you want. To pass additional variable to your serializer you can use context. Check this link to how use it.
In your case you should use this code:
self.get_serializer(data=data_to_save, context={'myArg': pharmacy})
And you already write the code in PharmacyUserSerializer to get myArg.

DRF YASG Customizing

I'm trying to customize my api documentation buuild with yasg.
First off, I would like to determine the naming of my own sections, and what endpoints should be included in this section. It seems that the naming of sections is based on the first prefix not belonging to the longest common prefix e.g.:
if we have the urls api/v1/message and api/v1/test than the sections will be named message and test. Is there a way for me to determine A custom naming for this section?
Furthermore, the introduction of every section is empty, how do I add text here?
And last but not least, Stripe has these amazing section dividers how can I add these in drf yasg.
Currently, I'm using APIView and #swagger_auto_schema to define the documentation of my endpoint.
In the code below, you can see how to add more information to define your endpoint. I hope it helps you
##serializers.py
class CategorySerializer(serializers.ModelSerializer):
"""
Serializing Categories
"""
class Meta:
model = Category
fields = [
'id', 'name', 'slug'
]
read_only_fields = [
'slug',
]
##views.py
username_param = openapi.Parameter('username', in_=openapi.IN_QUERY, description='Username',
type=openapi.TYPE_STRING)
email = openapi.Parameter('email', in_=openapi.IN_QUERY, description='Email',
type=openapi.TYPE_STRING)
category_response = openapi.Response('response description', CategorySerializer)
class CategoryList(APIView):
permission_classes = [AllowAny]
#swagger_auto_schema(
manual_parameters=[username_param, email],
query_serializer=CategorySerializer,
responses = {
'200' : category_response,
'400': 'Bad Request'
},
security=[],
operation_id='List of categories',
operation_description='This endpoint does some magic',
)
def get(self, request, format=None):
"""
GET:
Return a list of all the existing categories.
"""
categories = Category.objects.all()
serializer = CategorySerializer(categories, many=True)
return Response(serializer.data)
#swagger_auto_schema(
request_body=CategorySerializer,
query_serializer=CategorySerializer,
responses={
'200': 'Ok Request',
'400': "Bad Request"
},
security=[],
operation_id='Create category',
operation_description='Create of categories',
)
def post(self, request, format=None):
"""
POST:
Create a new category instance.
"""
serializer = CategorySerializer(data=request.data)
if serializer.is_valid():
serializer.save(created_by=self.request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
By last, if you want to see your endpoints in groups by link, you can test commenting the line below in yours urls.py
#urlpatterns = format_suffix_patterns(urlpatterns)
Below, some screen of how you should see it
You can find more information in the following link: https://drf-yasg.readthedocs.io/en/stable/custom_spec.html