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)
Related
This is my serializer:
class MetaDataSerializer(serializers.Serializer):
bg_colors = ColorSerializer(Color.objects.all(), many=True)
button_choices = serializers.SerializerMethodField()
class Meta:
fields = ('bg_colors', 'button_choices')
def get_button_choices(self, obj):
return {
'save': 1, 'continue': 2, 'cancel': 3, 'back': 4
}
I'm calling this serializer from my view like this:
class MetaDataView(RetrieveAPIView):
serializer_class = MetaDataSerializer
def get(self, request, *args, **kwargs):
return Response(self.get_serializer().data)
In the response I'm getting only the bg_colors field. The other field is absent from response, and its get_field method is also not called.
What am I doing wrong here?
Instead of passing the Colors queryset directing in serializer definition, pass it from your viewset.
Make this changes:
In serializer :-
class MetaDataSerializer(serializers.Serializer):
bg_colors = ColorSerializer(many=True)
button_choices = serializers.SerializerMethodField()
class Meta:
fields = ('bg_colors', 'button_choices')
def get_button_choices(self, obj):
return {
'save': 1, 'continue': 2, 'cancel': 3, 'back': 4
}
In view :-
class MetaDataView(RetrieveAPIView):
serializer_class = MetaDataSerializer
def get(self, request, *args, **kwargs):
final_data = {"bg_colors": Colors.objects.all()}
return Response(self.get_serializer(final_data).data)
You're not validating your data, you can only access the serializer.initial_data right now, which does not include the custom serializer fields that you've added. You must make a serializer instance first and then check if serializer.is_valid() and then you'll be able to access the custom serializer fields. Remember you can access the serializer data before validating by using serializer.initial_data (here serializer is the serializer instance name), after validation and before saving or inside if serializer.is_valid(): as serializer.validated_data and after saving the validated data as serializer.data
serializer = MetaDataSerializer(request.data)
print(serializer.initial_data)
if serializer.is_valid(raise_exception=True):
print(serializer.validated_data)
serializer.save()
print(serializer.data)
return Response({'data': serializer.data}, status=status.HTTP_200_OK)
now if it got validated without raising any errors then serializer.data will have data saame as the validated_data and if it does not get validate then it will have data same as serializer.initial_data
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.
I'm building an API based on a RetrieveUpdateAPIView with a Writable nested serializers. The issue I have is that when I make a PUT request, the response data contains all the data of the serializer... And the amount of those data is pretty big.
How can I modify my code to avoid this behavior? I imagine that this should be at the level of the view? I would preferably stick to generic views.
Thanks for your support.
For the record,
Here is the serializer code:
# DatetimeValue serializer
class DatetimeValueSerializer(serializers.ModelSerializer):
class Meta:
model = DatetimeValue
fields = ['date_time', 'value']
# Serializer that includes time series header and date time values
class TimeSeriesValueSerializer(serializers.ModelSerializer):
values = DatetimeValueSerializer(many=True)
class Meta:
model = TimeSeries
fields = ['id', 'name', 'values']
def update(self, instance, validated_data):
ts_name = validated_data.pop('name')
try:
time_series = TimeSeries.objects.get(name=ts_name)
except ObjectDoesNotExist:
raise ValidationError(_(f"Time series doesn't exist"))
values_data = validated_data.pop('values')
for datetime_value in values_data:
try:
DatetimeValue.objects.create(time_series=time_series, **datetime_value)
except IntegrityError:
raise ValidationError(_(f"ERROR in 'date_time' and/or 'value' provided. "
f"'date_time' has to be a correct date time format and 'value' a float. "
f"A 'date_time' has to be unique for a given time series"))
return time_series
And the view code:
# View to get time series datetime values
class TimeSeriesValueDetail(generics.RetrieveUpdateAPIView):
queryset = TimeSeries.objects.all()
serializer_class = TimeSeriesValueSerializer
You can modify the serialzier_class depending on request.method using get_serializer_class
class TimeSeriesValueDetail(generics.RetrieveUpdateAPIView):
queryset = TimeSeries.objects.all()
serializer_class = TimeSeriesValueSerializer
def get_serializer_class(self):
if self.request.method == 'PUT':
return ShortTimeSeriesSerializer
return super().get_serializer_class()
or you can return a custom data at the end of the put handler:
class TimeSeriesValueDetail(generics.RetrieveUpdateAPIView):
def put(self, request, *args, **kwargs):
self.update(request, *args, **kwargs)
return Response(data={'success':True})
or you can return some other data after the update:
class TimeSeriesValueDetail(generics.RetrieveUpdateAPIView):
...
def update(self, request, *args, **kwargs):
super().update(request, *args, **kwargs)
return Response(data={'success':True})
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
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.