AttributeError: This QueryDict instance is immutable for test cases - python

I am trying to change my request.data dict to remove some additional field.
It is working completely fine in views.
But when I run test cases for the same, I get this error:
AttributeError: This QueryDict instance is immutable
Here is my viewset:
def create(self, request, *args, **kwargs):
context = {'view': self, 'request': request}
addresses = request.data.pop("addresses", None)
serializer = self.get_serializer(data=request.data, context=context)
serializer.is_valid(raise_exception=True)
response = super(WarehouseViewSet, self).create(request, *args, **kwargs)
if addresses is None:
pass
else:
serializer = self.get_serializer(data=request.data, context=context)
serializer.is_valid(raise_exception=True)
addresses = serializer.update_warehouse_address(request, addresses, response.data["id"])
response.data["addresses"] = addresses
return Response(data=response.data, status=status.HTTP_201_CREATED)
and here is my test case for the same view:
def test_create_warehouse_authenticated(self):
response = client.post(
reverse('warehouse_list_create'),
data={
'name': self.test_warehouse['test_warehouse']['name'],
'branch': self.test_warehouse['test_warehouse']['branch'],
},
**{'HTTP_AUTHORIZATION': 'Bearer {}'.format(
self.test_users['test_user']['access_token']
)},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
How to fix this error?

Try setting format='json' when calling client.post, rather than relying on the default. You don't mention which test client you are using, but you should be using the APIClient
client = APIClient()
client.login(...)
client.post(..., format='json')
Newer Django has a immutable QueryDict, so this error will always happen if you are getting your data from querystring or a multipart form body. The test client uses multipart by default, which results in this issue.
Last Resort: If you need to post multipart, and also modify the query dict (very rare, think posting image + form fields) you can manually set the _mutable flag on the QueryDict to allow changing it. This is
setattr(request.data, '_mutable', True)

Related

Why POST API request not working from browser but working fine on Postman? Throws AssertionError in browser to call is_valid() before accessing .data

Ok, Here's the problem: I have created a POST API in Django REST framework & I want to add logged-in user to request.data when making POST request & It works perfectly fine when I create post calling this API from Postman, but when I visit the post create API endpoint from browser it throws AssertionError: '''When a serializer is passed a data keyword argument you must call .is_valid() before attempting to access the serialized .data representation.
You should either call .is_valid() first, or access .initial_data'''.
Here's the snippet of my get_serializer method which i use to override & add user to request.data:
class PostCreateView(CreateAPIView):
serializer_class = PostSerializer
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs["context"] = self.get_serializer_context()
request_data = self.request.data.copy()
request_data["user"] = self.request.user.id
kwargs["data"] = request_data
return serializer_class(*args, **kwargs)
def post(self,request):
serializer = self.get_serializer()
serializer.is_valid(raise_exception=True)
serializer.save()
status_code = status.HTTP_201_CREATED
response = {
'success' : 'True',
'status code' : status_code,
'message': 'Post created successfully',
'post_detail': serializer.data,
}
return Response(response, status=status_code)
Update: I have changed my code in post method to pass user initially in a Post instance & then pass it in PostSerializer as: serializer = self.serializer_class(post, data=request.data) & it works on both Postman as well as browser's DRF request. But I'm curious to know why my above code with get_serializer method gave me AssertionError from browser's DRF request? but worked fine when making a POST request from Postman.

Test Django with dictionary in dictionary data

Description
I'm using Django for my website and trying to write some unit tests. However, when using dict in dict data I met some errors. I found out that view received slightly different data.
From
{'class_id': '1', 'filter_options': {'full_name': 'user 1'}} in data of test file
to <QueryDict: {'class_id': ['1'], 'filter_options': ['full_name']}>. I means that value has changed to array, and value of nested dict wasn't detected.
Code
# test.py
def test_search_with_full_name(self):
data = {'class_id': '1', 'filter_options': {'full_name': 'user 1'}}
response = self.client.post('/api/students', data, format='json')
self.assertEqual(response.status_code, 200)
# view.py
class SearchStudentView(generics.GenericAPIView):
def post(self, request):
print(request.data)
if not 'class_id' in request.data:
return HttpResponse('class_id is required', status=400)
class_id = request.data['class_id']
students = Users.objects.filter(
details_student_attend_class__course__pk=class_id)
if 'filter_options' in request.data:
filter_options = request.data['filter_options']
if 'full_name' in filter_options:
students = students.filter(
full_name=filter_options['full_name'])
Thanks for any help!
Django's TestCase uses django.test.Client as the client and it does not set the content type etc. according to the format kwarg, instead it uses the content_type kwarg so you should either set that or use APITestCase from DRF which will use the APIClient class.
While using TestCase:
def test_search_with_full_name(self):
data = {'class_id': '1', 'filter_options': {'full_name': 'user 1'}}
response = self.client.post('/api/students', data, content_type='application/json')
self.assertEqual(response.status_code, 200)
While using APITestCase from DRF:
from rest_framework.test import APITestCase
class YourTest(APITestCase):
def test_search_with_full_name(self):
data = {'class_id': '1', 'filter_options': {'full_name': 'user 1'}}
response = self.client.post('/api/students', data, format='json')
self.assertEqual(response.status_code, 200)
Could you change format to content_type='application/json'
Django docs say this
If you provide content_type as application/json, the data is serialized using json.dumps() if it’s a dict, list, or tuple. Serialization is performed with DjangoJSONEncoder by default, and can be overridden by providing a json_encoder argument to Client. This serialization also happens for put(), patch(), and delete() requests.
There might be an issue with serializing the dictionaries as values for POST fields. So in case if above solution doesn't work another way could be a serialize filter options by your self using json.dumps(filter_options) before passing to the request. And then read them via json.loads(request.POST.get(filter_options)) in your view.

patch() got an unexpected keyword argument

I'm trying to write a test in which an object is updated using patch.
class Search(models.Model):
id_search = models.AutoField(primary_key=True)
id_user = models.IntegerField(null=False)
.
.
archive = models.BooleanField(default=False)
def test_archive_search(self):
user = User(id_user=75720912,
login='Client:75720912',
)
user.save()
search = Search(
id_user=75720912,
.
.
archive=False
)
search.save()
url = reverse('search-update', kwargs={'id_search':1})
data = {'archive': True}
response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
url(r'^search-update/(?P<id_search>\d+)$', SearchUpdateView.as_view(), name='search-update')
class SearchUpdateView(generics.UpdateAPIView):
serializer_class = SearchSerializer
def get_object(self,id_search):
return Search.objects.get(id_search=id_search)
def patch(self, request):
id_search = self.request.query_params.get('id_search', None)
search_object = self.get_object(id_search=id_search)
serializer = SearchSerializer(search_object, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST)
and get this error:
TypeError: patch() got an unexpected keyword argument 'id_search'
Interesting thing is that when the url was:
url(r'^search-update/$', SearchUpdateView.as_view(), name='search-update')
SearchUpdateView worked properly with given query params.
EDIT
I discovered that passing id_search to patch in view solves this problem when it comes to test, but it spoils working view.
class SearchUpdateView(generics.UpdateAPIView):
serializer_class = SearchSerializer
def get_object(self, id_search):
return Search.objects.get(id_search=id_search)
def patch(self, request, id_search):
#id_search = self.request.query_params.get('id_search', None)
search_object = self.get_object(id_search=id_search)
serializer = SearchSerializer(search_object, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST)
Still I've got no idea how to bring it together.
if you define the url that way, the patch method will get the id_search param as a keyword argument, as the error says.
Instead, you are retrieving it as if it came as a query param, i.e. not part of the url path but as search-update?id_search=.
Given you are passing None as a default when getting it, it works when you omit it.
So choose which way you want to go.
In case the url definition is correct, then add the id_search argument to the signature of the patch method
and remove the code that retrieves it manually.
Or do both, as suggested in the comments above, by assigning a default value of None to the argument and retrieving it from the request if it is not part of the path

DRF: how to validate arguments passed to serializer.save()?

How can I validate an additional arguments passed like this:
class MyViewSet(MultiSerializerViewSet):
# some stuff
def perform_create(self, serializer):
serializer.save(creator=self.request.user)
How can I validate a creator in the serializer?
You can not validate fields passed as arguments to serializer.save() method, they will only be available in create method of the serializer, and I suggest not to run validations there. What I do in these kind of situations is, I override the create method of the viewset, and add extra parameters to the data I pass to the serializer.
class MyViewSet(MultiSerializerViewSet):
def create(self, request, *args, **kwargs):
request_data = request.data
request_data['creator'] = self.user.id
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)
With this setup, you also need to add creator as a serializer field. With this, the field will be alailable in validtion flow.
Django reset framework has is_valid() method validate serializer
for e.g.
serializer = YourSerializer(data={'sample1': 'foobar', 'sample2': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'sample1': ['Some error.'], 'sample2': ['Some error.']}
refer this

Why does context={'request': self.request} need in serializer?

Today I dig into django-rest-auth package a bit. And I have no idea what context={'request': self.request} for in get_response and post function.
They say context parameter is for including extra context to serializer. But below the codes, it seems not necessary to put context parameter. Is there something I have missed?
context: http://www.django-rest-framework.org/api-guide/serializers/
django-rest-auth : https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/views.py
def get_response(self):
serializer_class = self.get_response_serializer()
if getattr(settings, 'REST_USE_JWT', False):
data = {
'user': self.user,
'token': self.token
}
serializer = serializer_class(instance=data,
context={'request': self.request})
else:
serializer = serializer_class(instance=self.token,
context={'request': self.request})
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
self.request = request
self.serializer = self.get_serializer(data=self.request.data,
context={'request': request})
self.serializer.is_valid(raise_exception=True)
self.login()
return self.get_response()
Sometimes you need request's data inside serializer method. For this case you can provide request to serializer's context. For example if you look into PasswordResetSerializer you'll see in save method use_https option which calculated based on the request passed with context argument:
def save(self):
request = self.context.get('request')
# Set some values to trigger the send_email method.
opts = {
'use_https': request.is_secure(),
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
'request': request,
}
Also you can check if user is authenticated or not and depends on it return one data or another on serializer level.

Categories