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

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.

Related

How do you perform get_or_create on DRF ModelViewSet when over riding create()?

I have a ModelViewSet with create() method overwritten as I may be creating more than one instance at a time. I was wondering if it is possible to perform a get_or_create instead of .perform_create() as seen below?
Essentially if I make the same call twice I would only like the first call to create the instance/s.
class CreateTokenView(viewsets.ModelViewSet):
queryset = BayAccessToken.objects.all()
serializer_class = accessTokenSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
Or is the best option to just loop through and perform own get_or_create on data?
You have to override the create method on the serializer, something like this:
class accessTokenSerializer(serializer.X):
def create(self, validated_data):
return YourModel.objects.get_or_create(name=validated_data.pop("name"), defaults=validated_data)[0]

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)

When and how to validate data with Django REST Framework

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

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.

Dynamically include or exclude Serializer class fields

In my User profile model I've included a show_email field explicitly. So, to add this feature to my API, the UserSerializer class looks like this:
class UserSerializer(serializers.ModelSerializer):
email = serializers.SerializerMethodField('show_email')
def show_email(self, user):
return user.email if user.show_email else None
class Meta:
model = django.contrib.auth.get_user_model()
fields = ("username", "first_name", "last_name", "email")
But I don't really like it. I think it would be a lot cleaner if the field email would be completely excluded from the serializer output it show_email is False, instead showing that ugly "email": null thing.
How could I do that?
You could do this in your API view by overriding the method returning the response, i.e. the "verb" of the API view. For example, in a ListAPIView you would override get():
class UserList(generics.ListAPIView):
model = django.contrib.auth.get_user_model()
serializer_class = UserSerializer
def get(self, request, *args, **kwargs):
response = super(UserList, self).get(request, *args, **kwargs)
for result in response.data['results']:
if result['email'] is None:
result.pop('email')
return response
You would probably want to add some more checking for attributes, but that's the gist of how it could be done. Also, I would add that removing fields from some results may cause issues for the consuming application if it expects them to be present for all records.
This answer comes late but for future google searches: there is an example in the documentation about Dynamically modifying fields.
So, by passing an argument to the serializer, you control whether or not a field is processed:
serializer = MyPostSerializer(posts, show_email=permissions)
and then in the init function in the serializer you can do something like:
class MyPostSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
show_email = kwargs.pop('show_email', None)
# Instantiate the superclass normally
super(DeviceSerializer, self).__init__(*args, **kwargs)
if not show_email:
self.fields.pop("show_email")
Now the show_email field will be ignored by the serializer.
You could override restore_fields method on serializer. Here in restore_fields method you can modify list of fields - serializer.fields - pop, push or modify any of the fields.
eg: Field workspace is read_only when action is not 'create'
class MyPostSerializer(ModelSerializer):
def restore_fields(self, data, files):
if (self.context.get('view').action != 'create'):
self.fields.get('workspace').read_only=True
return super(MyPostSerializer, self).restore_fields(data, files)
class Meta:
model = MyPost
fields = ('id', 'name', 'workspace')
This might be of help...
To dynamically include or exclude a field in an API request, the modification of
#stackedUser's response below should do:
class AirtimePurchaseSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
try:
phone = kwargs['data']['res_phone_number']
except KeyError:
phone = None
# Instantiate the superclass normally
super(AirtimePurchaseSerializer, self).__init__(*args, **kwargs)
if not phone:
self.fields.pop("res_phone_number")
res_phone_number = serializers.CharField(max_length=16, allow_null=False)