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

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

Related

Django How to use drf-yasg in class-base view which is parent of all other views to generate swagger docs?

I'm using drf-yasg for swagger docs, but I'm using a BaseView which is the parent class view for all other views, thus all HTTP methods are written in BaseView so that children views don't need to implement these methods.
the problem is that when I added #swagger_auto_schema to baseView methods it didn't see the child attributes but the BaseView's attributes which are empty.
The BaseView:
class BaseView(APIView):
model = models.Model
serializer_class = serializers.Serializer
description = ''
id = openapi.Parameter('id', in_=openapi.IN_QUERY, type=openapi.FORMAT_UUID)
#swagger_auto_schema(manual_parameters=[id] ,responses={200: serializer_class},operation_description=description)
def get(self, request, id=None, **kwargs):
....
#swagger_auto_schema(request_body=serializer_class)
def post(self, request, **kwargs):
....
Then let's say I have this child view:
class TestApi(BaseView):
model = test
description = 'This is test api'
serializer_class = TestSerializer
In this approach Swagger is showing empty values for description and serializer_class because it doesn't see child values how to solve this without having to configure each view with HTTP methods and #swagger_auto_schema?
Thanks in advance.
Create a python file named api_docs.py and write a description of your API View of all methods.
docs = {
'View Name': {
'post': 'your description',
'list': '....description',
}
}
after that you have to import this into your view and add this.
from django.utils.decorators import method_decorator
#method_decorator(name="HTTP method name like post put destroy", decorator=swagger_auto_schema(
tags=["App name"], operation_description=docs['ViewName']['post']))
if you are overriding any method then you have to explicitly add on
that view like this
#swagger_auto_schema(tags=["App name"], operation_description=docs['View Name']['post'])
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

How to add suffix url in Django Rest Framework?

How to add suffix url in ModelViewSet
Serializer
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
def update(self, instance, validated_data):
...
...
ModelViewSet
I'm doing a custom partial update
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
http_method_names = ['get', 'patch', 'head', 'options']
def partial_update(self, request, *args, **kwargs):
super(CommentViewSet, self).partial_update(
request, *args, **kwargs)
return Response({
"data": request.data,
...
...
})
Urls
router = routers.DefaultRouter()
router.register(
"comments",
CommentViewSet
)
urlpatterns = [
path('api/v1/', include(router.urls))
]
Currently have this, but I want to add a suffix
url: http://localhost:8000/api/v1/comments/{id}
I want to do something like this
url: http://localhost:8000/api/v1/comments/{id}/update_or_whatever
What you want to do does not follow the REST architecture and popular practice. In REST, each endpoint represents a resource. The actions on the resource are represented by HTTP methods. So if you have the comments resource accessible through this url http://localhost:8000/api/v1/comments/, you can create (POST), get the list (GET) on the list endpoint and edit(PUT and PATCH), fetch a single comment (GET) and delete(DELETE) using the detail endpoint. In this way, you don't need to explicitly name the URL according to the action like http://localhost:8000/api/v1/comments/{id}/update. This is the architecture that DRF is built on and hence why you have this url style. Of course, there are actions like login and others that may not fit into this architecture and that's why DRF provides custom actions. But you should not use it to override the default actions mapped to HTTP methods
Another magic from DFR
https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions
Only change what u need in the view and add this action decorator.
In your views.py
#action(methods=['get'], detail=True, permission_classes=[IsAuthenticated])
def get_file(self, request, pk=None):
if pk is None:
raise ValueError("Found empty filename")
obj = self.get_queryset().filter(pk=pk).first()
if obj and obj.image_file:
return FileResponse(obj.image_file, content_type="image/jpeg")
return Response(
'Nothing to show',
status=status.HTTP_400_BAD_REQUEST)

AttributeError: This QueryDict instance is immutable for test cases

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)

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

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