When and how to validate data with Django REST Framework - django

I have a model which is exposed as a resource with Django REST Framework.
I need to manually create the objects when a POST requests is performed on the related endpoints, that why I use a generics.ListCreateAPIView and override the create() method.
However I need to check that the parameters given in the payload of the POST request are well-formed/existing/etc...
Where shall I perform this validation, and how is it related with the Serializer?
I tried to write a validate() method in the related Serializer, but it is never called on POST requests.
class ProductOrderList(generics.ListCreateAPIView):
model = ProductOrder
serializer_class = ProductOrderSerializer
queryset = ProductOrder.objects.all()
def create(self, request, *args, **kwargs):
data = request.data
# Some code here to prepare the manual creation of a 'ProductOrder' from the data
# I would like the validation happens here (or even before)
po = ProductOrder.objects.create(...)
class ProductOrderSerializer(serializers.ModelSerializer):
class Meta:
model = ProductOrder
def validate(self, data): # Never called
# Is it the good place to write the validator ??

Here's the implementation of the create method that you overrided, taken from the mixins.CreateModelMixin class:
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)
As you can see, it gets the serializer, validates the data and performs the creation of the object from the serializer validated data.
If you need to manually control the creation of the object, perform_create is the hook that you need to override, not create.
def perform_create(self, serializer):
# At this, the data is validated, you can do what you want
# by accessing serializer.validated_data

Related

DRF How to perform additional operations on an instance

Which of the given ways of writing additional logic is technically correct? The example includes changing the status of the document. After a quick reserach of similar questions, I understand that there are 3 possibilities described below. However, no answer describes which solution is used during daily practicals, and the examples from the documentation do not dispel doubts. Please help.
Providing custom data to the serializer and the standard model serializer:
class PZSaveAPIView(APIView):
#transaction.atomic
def patch(self, request, pk, format=None):
document = get_object_or_404(PZ, pk=pk)
print(request.data)
serializer = PZModelSerializer(
document, data={'status': 'E'}, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
After the object is serialized, change the status:
class PZSaveAPIView(APIView):
#transaction.atomic
def patch(self, request, pk, format=None):
document = get_object_or_404(PZ, pk=pk)
serializer = PZModelSerializer(
document, data=request.data, partial=True)
if serializer.is_valid():
pz = serializer.save()
pz.status = 'S'
pz.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Contain the logic in the serializer, and the view stays basic:
class PZUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = PZ
fields = '__all__'
def update(self, instance, validated_data):
instance.status = 'S'
instance.save()
return instance
Is it even necessary to use a serializer in such cases? Example:
class PZSaveAPIView(APIView):
def patch(self, pk):
document = get_object_or_404(PZ, pk=pk)
document.set_status_saved()
document.save()
return Response('Document saved')
If there is a need to validate data and return those data to the front end, the serializer is definitely required. So, whether to use a serializer or not depends upon the case, it's okay not to use a serializer if it is not in need.
And about whether to put logic on views or serializer, Django books recommend thick serializer and thin views. And Django rest framework itself provides update and create methods in ModelSerializer which mean it prefers the logic to update and create inside serializer and views to just return response.

Why is custom serializer save() method not called from view?

I have a view like this:
class MyViewSet(ModelViewSet):
# Other functions in ModelViewset
#action(methods=['post'], url_path='publish', url_name='publish', detail=False)
def publish_data(self, request, *args, **kwargs):
new_data = util_to_filter_data(request.data)
serializer = self.serializer_class(data=new_data, many=True)
serializer.is_valid(raise_exception=True)
serializer.save() # This is calling django's save() method, not the one defined by me
return Response(serializer.data)
And my serializer:
class SomeSerializer(ModelSerializer):
# fields
def create(self, validated_data):
print("in create")
return super().create(validated_data)
def save(self, **kwargs):
print("in save")
my_nested_field = self.validated_data.pop('my_nested_field', '')
# Do some operations on this field, and other nested fields
obj = None
with transaction.atomic():
obj = super().save(kwargs)
# Save nested fields
return obj
When I'm calling my serializer's save() method from my view (see the comment in view code), the save() method of django is called instead of the one defined by me, due to which I'm having issues that were handled in my save() method.
This is the traceback:
File "/home/ubuntu18/Public/BWell/path_to_view/views.py", line 803, in publish_data
serializer.save()
File "/home/ubuntu18/Envs/bp_env/lib/python3.7/site-packages/rest_framework/serializers.py", line 720, in save
self.instance = self.create(validated_data)
I wanted to perform some actions in save() before calling create(), but my flow is not reaching the save() method.
What am I doing wrong here?
P.S.: This is happening only when I am providing many=True in serializer constructor.
It is giving this error.
Serializers with many=True do not support multiple update by default, only multiple create.
For updates it is unclear how to deal with insertions and deletions. If you need to support multiple update, use a SomeSerializer class and override .update() so you can specify the behavior exactly.

How to create multiple instances in DRF?

I have a list of data coming in a request, and after filtering taken out data that needs creation, I'm passing it to the serializer create method, but I'm getting this error:
AssertionError: The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `apps.some_app.serializers.SomeSerializer`, or set `read_only=True` on nested serializer fields.
My view looks like this:
class MyViewSet(ModelViewSet):
# Other functions in ModelViewset
#action(methods=['post'], url_path='publish', url_name='publish', detail=False)
def publish_data(self, request, *args, **kwargs):
new_data = util_to_filter_data(request.data)
serializer = self.serializer_class(data=new_data, many=True)
if serializer.is_valid(raise_exception=True):
serializer.create(serializer.validated_data)
return Response()
I understood the error, that I am passing nested fileds in the create method. But, when I am directly calling my Viewset with single POST request, it is created successfully, even though it too contains nested fields.
What am I doing wrong here?
Here's my serializer:
class SomeSerializer(ModelSerializer):
# fields
def create(self, validated_data):
print("in create")
return super().create(validated_data)
def save(self, **kwargs):
print("in save")
my_nested_field = self.validated_data.pop('my_nested_field', '')
# Do some operations on this field, and other nested fields
obj = None
with transaction.atomic():
obj = super().save(kwargs)
# Save nested fields
return obj
Here in create is being seen in terminal, but not in save.
you should not use serializer create method directly; instead use save. Also no need to check serializer.is_valid with if when raise_exception=True, if it is not valid it will return the exception
Try following.
class MyViewSet(ModelViewSet):
# Other functions in ModelViewset
#action(methods=['post'], url_path='publish', url_name='publish', detail=False)
def publish_data(self, request, *args, **kwargs):
new_data = util_to_filter_data(request.data)
serializer = self.serializer_class(data=new_data, many=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)

Triggering a django rest framework update() function using HTTP Requests

I just started to learn django last week so please excuse my ignorance if I'm completely approaching this problem the wrong way.
So I've been following a thinkster tutorial on setting up a User model that allows the change of a password in the model. So far I have a url (/api/user) that leads to this view:
class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (UserJSONRenderer,)
serializer_class = UserSerializer
def retrieve(self, request, *args, **kwargs):
#turns the object recieved into a JSON object
serializer = self.serializer_class(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
def update(self, request, *args, **kwargs):
serializer_data = request.data
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
I understand that this section :
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
will call upon a serializer class along the lines of:
class UserSerializer(serializers.ModelSerializer):
#This class handles serialization and deserialization of User objects
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True
)
class Meta:
model = User
fields = ('email', 'username', 'password', 'token',)
read_only_fields=('token',)
def update(self, instance, validated_data):
#performs an update on the user
password = validated_data.pop('password', None)
#have to take out password because setattr does not handle hashing etc
for (key, value) in validated_data.items():
#for the keys after taking out password set them to the updating User instance
setattr(instance, key, value)
if password is not None:
instance.set_password(password) #set_password is handled bydjango
instance.save() #set_password does not save instance
return instance
again I understand this section will essentially take request.data and "update" the model. However I'm stuck on how to test this feature using Postman.
Currently when I send a GET request to the URL using Postman I get this response:
GET Request result
The response is based off of my authenticate class that uses JWT authentication.
My question is, how do I trigger that update function using a Postman HTTP Request.
PATCH(partial_update) or PUT(update) http://127.0.0.1:8000/api/user/user_id/
you can see router table here

How to make a PATCH request using DJANGO REST framework

I am not very experience with Django REST framework and have been trying out many things but can not make my PATCH request work.
I have a Model serializer. This is the same one I use to add a new entry and ideally I Would want to re-use when I update an entry.
class TimeSerializer(serializers.ModelSerializer):
class Meta:
model = TimeEntry
fields = ('id', 'project', 'amount', 'description', 'date')
def __init__(self, user, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
super(TimeSerializer, self).__init__(*args, **kwargs)
self.user = user
def validate_project(self, attrs, source):
"""
Check that the project is correct
"""
.....
def validate_amount(self, attrs, source):
"""
Check the amount in valid
"""
.....
I tried to use a class based view :
class UserViewSet(generics.UpdateAPIView):
"""
API endpoint that allows timeentries to be edited.
"""
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
My urls are:
url(r'^api/edit/(?P<pk>\d+)/$', UserViewSet.as_view(), name='timeentry_api_edit'),
My JS call is:
var putData = { 'id': '51', 'description': "new desc" }
$.ajax({
url: '/en/hours/api/edit/' + id + '/',
type: "PATCH",
data: putData,
success: function(data, textStatus, jqXHR) {
// ....
}
}
In this case I would have wanted my description to be updated, but I get errors that the fields are required(for 'project'and all the rest). The validation fails. If add to the AJAX call all the fields it still fails when it haves to retrieve the 'project'.
I tried also to make my own view:
#api_view(['PATCH'])
#permission_classes([permissions.IsAuthenticated])
def edit_time(request):
if request.method == 'PATCH':
serializer = TimeSerializer(request.user, data=request.DATA, partial=True)
if serializer.is_valid():
time_entry = serializer.save()
return Response(status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST)
This did not work for partial update for the same reason(the validation for the fields were failing) and it did not work even if I've sent all the fields. It creates a new entry instead of editing the existing one.
I would like to re-use the same serializer and validations, but I am open to any other suggestions.
Also, if someone has a piece of working code (ajax code-> api view-> serializer) would be great.
class DetailView(APIView):
def get_object(self, pk):
return TestModel.objects.get(pk=pk)
def patch(self, request, pk):
testmodel_object = self.get_object(pk)
serializer = TestModelSerializer(testmodel_object, data=request.data, partial=True) # set partial=True to update a data partially
if serializer.is_valid():
serializer.save()
return JsonResponse(code=201, data=serializer.data)
return JsonResponse(code=400, data="wrong parameters")
Documentation
You do not need to write the partial_update or overwrite the update method. Just use the patch method.
Make sure that you have "PATCH" in http_method_names. Alternatively you can write it like this:
#property
def allowed_methods(self):
"""
Return the list of allowed HTTP methods, uppercased.
"""
self.http_method_names.append("patch")
return [method.upper() for method in self.http_method_names
if hasattr(self, method)]
As stated in documentation:
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the partial argument in order to allow partial updates.
Override update method in your view:
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = TimeSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save(customer_id=customer, **serializer.validated_data)
return Response(serializer.validated_data)
Or just override method partial_update in your view:
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
Serializer calls update method of ModelSerializer(see sources):
def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data)
# Simply set each attribute on the instance, and then save it.
# Note that unlike `.create()` we don't need to treat many-to-many
# relationships as being a special case. During updates we already
# have an instance pk for the relationships to be associated with.
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
Update pushes the validated_data values to the given instance. Note that update should not assume all the fields are available. This helps to deal with partial updates (PATCH requests).
The patch method is worked for me using viewset in DRF. I'm changing you code:
class UserViewSet(viewsets.ModelViewSet):
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
def perform_update(self, serializer):
user_instance = serializer.instance
request = self.request
serializer.save(**modified_attrs)
return Response(status=status.HTTP_200_OK)
Use ModelViewSet instead and override perform_update method from UpdateModelMixin
class UserViewSet(viewsets.ModelViewSet):
queryset = TimeEntry.objects.all()
serializer_class = TimeSerializer
def perform_update(self, serializer):
serializer.save()
# you may also do additional things here
# e.g.: signal other components about this update
That's it. Don't return anything in this method. UpdateModelMixin has implemented update method to return updated data as response for you and also clears out _prefetched_objects_cache. See the source code here.
I ran into this issues as well, I solved it redefining the get_serializer_method and adding custom logic to handle the partial update. Python 3
class ViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == "partial_update":
return PartialUpdateSerializer
Note: you may have to override the partial_update function on the serializer. Like so:
class PartialUpdateSerializer(serializers.Serializer):
def partial_update(self, instance, validated_data):
*custom logic*
return super().update(instance, validated_data)
Another posiblity is to make the request by URL. For example, I have a model like this
class Author(models.Model):
FirstName = models.CharField(max_length=70)
MiddleName = models.CharField(max_length=70)
LastName = models.CharField(max_length=70)
Gender = models.CharField(max_length=1, choices = GENDERS)
user = models.ForeignKey(User, default = 1, on_delete = models.CASCADE, related_name='author_user')
IsActive = models.BooleanField(default=True)
class Meta:
ordering = ['LastName']
And a view like this
class Author(viewsets.ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorSerializer
So can enter http://127.0.0.1:8000/author/ to get or post authors. If I want to make a PATCH request you can point to http://127.0.0.1:8000/author/ID_AUTHOR from your client. For example in angular2, you can have something like this
patchRequest(item: any): Observable<Author> {
return this.http.patch('http://127.0.0.1:8000/author/1', item);
}
It suppose you have configured your CORS and you have the same model in back and front.
Hope it can be usefull.