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

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,
})

Related

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', '')

Testing custom action on a viewset in Django Rest Framework

I have defined the following custome action for my ViewSet Agenda:
class AgendaViewSet(viewsets.ModelViewSet):
"""
A simple viewset to retrieve all the Agendas
"""
queryset = Agenda.objects.all()
serializer_class = AgendaSerializer
#action(detail=False, methods=['GET'])
def get_user_agenda(self, request, pk=None):
print('here1')
id = request.GET.get("id_user")
if not id:
return Response("No id in the request.", status=400)
id = int(id)
user = User.objects.filter(pk=id)
if not user:
return Response("No existant user with the given id.", status=400)
response = self.queryset.filter(UserRef__in=user)
if not response:
return Response("No existant Agenda.", status=400)
serializer = AgendaSerializer(response, many=True)
return Response(serializer.data)
Here, I'd like to unit-test my custom action named "get_user_agenda".
However, when I'm testing, the debug output("here1") doesn't show up, and it always returns 200 as a status_code.
Here's my test:
def test_GetUserAgenda(self):
request_url = f'Agenda/get_user_agenda/'
view = AgendaViewSet.as_view(actions={'get': 'retrieve'})
request = self.factory.get(request_url, {'id_user': 15})
response = view(request)
self.assertEqual(response.status_code, 400)
Note that:
self.factory = APIRequestFactory()
Am I missing something?
Sincerely,
You will have to use the method name of the custom action and not retrieve so:
view = AgendaViewSet.as_view(actions={'get': 'get_user_agenda'})
You have to specify request url
#action(detail=False, methods=['GET'], url_path='get_user_agenda')
def get_user_agenda(self, request, pk=None):
And in my opinion it would be better to use detail=True, and get pk from url.
For example: 'Agenda/pk_here/get_user_agenda/'

django test client.put doesn't have data

I am trying to test django view with REST framework.
It starts like this.
class MoveViewSet(viewsets.ModelViewSet):
serializer_class = FamilySerializer
queryset = Family.objects.all()
http_method_names = ['put']
def update(self, request, pk=None):
user = request.mobile_user
...
family_id = request.POST.get('family_id', None)
...
in test.py I make a request like this.
data = dict(
join_type='join',
family_id=target_family.id,
)
# also tried data = { ... }
header = {
'Authorization': user.api_key,
...
}
client = Client()
response = client.put(url, data=data, content_type='x-www-form-urlencoded', **header)
### What I've tried ###
# response = client.put(url, data=data, **header)
# response = self.client.put(url, data=data, **header)
# response = client.put(url, data=data, content_type='application/json', **header)
but in view, request.POST.get('paramname', 'default') makes error. request.POST has empty parameter set and of course, request.PUT is None.
I checked middleware but neither could I find params there.
Also I've tried this in middleware's process_request.
def process_request(self, request):
if request.method == "PUT" and request.content_type != "application/json":
if hasattr(request, '_post'):
del request._post
del request._files
try:
request.method = "POST"
request._load_post_and_files()
request.method = "PUT"
except AttributeError as e:
request.META['REQUEST_METHOD'] = 'POST'
request._load_post_and_files()
request.META['REQUEST_METHOD'] = 'PUT'
request.PUT = request.POST
It gives me this error.
AttributeError: 'WSGIRequest' object has no attribute 'content_type'
If I send client.post() with data, request.POST has data, but put doesn't.
How could I test client.put() with parameters?
request's content_type attribute has been added in Django 1.10. Since you are using Django 1.9 you cannot use this attribute. You can chec request.META.get('HTTP_ACCEPT') solution as mentioned here or update Django.

How Configure Django Rest Framework to return errors with custom text

I want to customize the JSON response when adding a new item to the database it returns the following.
HTTP 400 BAD REQUEST
Content-Type: application/json Vary:
Accept Allow: POST, OPTIONS
{
"nick": [
"Users with this Nick already exists."
]
}
and
{
"nick": [
"Your username is empty"
]
}
I want it to return (This username already exists, please use a different one.)
or
"Username %s already exists", (self.nick)
I used the following sample but does not work if the value is empty or invalid.
def validate_title(self, attrs, source):
"""
Check that the blog post is about Django.
"""
value = attrs[source]
if "django" not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return attrs
this is the JSON that gets sent to the API.
{
"name": "myname",
"nick":"",
"type_account":"1",
"email":"my-email#gmail.com",
"pass_field":"12345"
}
serializers.py
class userSerializer(serializers.ModelSerializer):
class Meta:
model = users
fields = ('nick', 'email', 'pass_field', 'type_account')
def validate_nick(self, attrs, source):
value = attrs[source]
if not value:
raise serializers.ValidationError('Username cannot be empty')
elif self.Meta.model.objects.filter(nick=value).exists():
raise serializers.ValidationError("Username "+value+" is in use")
return attrs
views.py
#api_view(['POST'])
def user_add(request):
"""
Saves a new user on the database
"""
if request.method == 'POST':
serializer = userSerializer(data=request.DATA)
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)
the answer to this question also adds the following as #Fiver answered
class userLoginSerializer(serializers.ModelSerializer):
nick = serializers.CharField(error_messages={'required':'Please Type a Username'})
pass_field = serializers.CharField(error_messages={'required':'Please Type a Password'})
class Meta:
model = users
fields = ('nick', 'pass_field')
I believe something like the following will work:
def validate_nick(self, attrs, source):
"""
Check that 'nick' is not already used or empty.
"""
value = attrs[source]
if not value:
raise serializers.ValidationError("Nick cannot be empty!")
elif self.Meta.model.objects.filter(nick=value).exists():
raise serializers.ValidationError("Username %s already exists", value)
return attrs
It should be possible to alter the error messages at the model level, but unfortunately REST Framework doesn't support that yet. Here is an issue dealing with the problem. It includes a suggested method for overriding the validator in the serializer.
You should use custom error handler. Follow here to setup.
Your Custom error handler should be like this:
def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if response is not None:
for field, value in response.data.items():
value = ''.join(value)
response.data = {} # Empty django's custom error
response.data['detail'] =value #customize it how you want
return response

Django REST framework post array of objects

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