I'm currently working on Django with Django Rest Framwork.
I can't update my object within nested object field.
serializer.py
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model = Owner
fields = ('id', 'name')
class CarSerializer(serializers.ModelSerializer):
owner = ownerSerializer(many=False, read_only=False)
class Meta:
model = Car
fields = ('id', 'name', 'owner')
view.py
class OwnerViewSet(viewsets.ModelViewSet):
queryset = Owner.objects.all()
serializer_class = OwnerSerializer
class CarViewSet(viewsets.ModelViewSet):
serializer_class = CarSerializer
queryset = Car.objects.all()
def create(self, request):
serialized = self.serializer_class(data=request.DATA)
if serialized.is_valid():
serialized.save()
return Response(status=HTTP_202_ACCEPTED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
When I do this :
Request URL:http://localhost:9000/api/v1/cars/1/?format=json
Request Method:PUT
Request Paylod :
{
"id":1,
"name": "TEST",
"ower": {
"id":1,
"name": "owner_test"
}
}
I get the following Response :
The `.update()` method does not support writable nestedfields by default.
Write an explicit `.update()` method for serializer `app.serializers.CarSerializer`,
or set `read_only=True` on nested serializer fields.
Knowing :
I want to keep the owner serialization on GET;
We can imagine the car nested by another object and ect...
How can I do if i want to change the owner when I update the car.
A little late, but, Try this,
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model = Owner
fields = ('id', 'name')
extra_kwargs = {
'id': {
'read_only': False,
'required': True
}
} #very important
def create(self, validated_data):
# As before.
...
def update(self, instance, validated_data):
# Update the instance
instance.some_field = validated_data['some_field']
instance.save()
# Delete any detail not included in the request
owner_ids = [item['owner_id'] for item in validated_data['owners']]
for owner in cars.owners.all():
if owner.id not in owner_ids:
owner.delete()
# Create or update owner
for owner in validated_data['owners']:
ownerObj = Owner.objects.get(pk=item['id'])
if ownerObje:
ownerObj.some_field=item['some_field']
....fields...
else:
ownerObj = Owner.create(car=instance,**owner)
ownerObj.save()
return instance
Just in-case someone stumbles on this
had the same error in my case but setting read_only to True fixed it for me.
owner = ownerSerializer(many=False, read_only=True)
Note that, this field won't appear in the form when posting data to the api.
Related
When I override the save() method, the create() method is called inside, but when I add a read-only field uuid, the field is not serialized.
serializer.py
class AwsUploadFileSerializer(serializers.ModelSerializer):
extract_file_name = serializers.CharField(source='extract_file.name', read_only=True)
class Meta:
model = ExtractAWSFile
fields = [
'uuid',
'extract_file',
'extract_file_name'
]
extra_kwargs = {
'extract_file': {'write_only': True}
}
def save(self, **kwargs):
instance: ExtractAWSFile = super().create(self.validated_data)
res = upload_file_to_aws(instance.extract_file.path)
if not res:
instance.extract_file.delete()
instance.delete()
return instance
response
{
"extract_file_name": "tets3.jpg"
}
So I'm trying to call the save() method of the parent class so that the uuid field can be serialized, but there is something wrong with the file name field I wrote earlier and it will take the path with it instead of just displaying the name.
serializer.py
class AwsUploadFileSerializer(serializers.ModelSerializer):
extract_file_name = serializers.CharField(source='extract_file.name', read_only=True)
class Meta:
model = ExtractAWSFile
fields = [
'uuid',
'extract_file',
'extract_file_name'
]
extra_kwargs = {
'extract_file': {'write_only': True}
}
def save(self, **kwargs):
instance = super().save(**kwargs)
res = upload_file_to_aws(instance.extract_file.path)
if not res:
instance.extract_file.delete()
instance.delete()
return instance
response
{
"uuid": "c4aea74e-3748-4c05-8d6c-2413b1eebcc6",
"extract_file_name": "extractAWS/2022/10/08/tets3.jpg"
}
Why is that? I'm wondering what I should do after save() to properly display the uuid field
def save(self, **kwargs):
res = upload_file_to_aws(instance.extract_file.path)
if not res:
instance.extract_file.delete()
instance.delete()
instance = super().save(**kwargs) #call the save method end of your code
I am using django-filter to filter the result query based on the user input.
Imagine that I have the following Post:
[
{
"id": 1,
"title": "salam",
"content": "chetori",
"is_draft": false,
"author": 1
}
]
I have created a filterset_class as following:
class PostFilter(filter.FilterSet):
class Meta:
model = Post
fields = {
'title': ['icontains'],
'content': ['icontains'],
}
whenever I use that icontains doesn't work properly, for example http://localhost:8000/posts/?content=chetorz query should returns empty list but it doesn't:
But it works whenever I add the following line to project settings.py:
FILTERS_DEFAULT_LOOKUP_EXPR = 'icontains'
I don't know how can I change the code to work properly without changing the default behavior of the LOOKUP_EXPR.
PS views.py:
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = [rest_filters.SearchFilter, filters.DjangoFilterBackend]
filterset_class = PostFilter
search_fields = ['title', 'content', 'author__user__username']
def get_permissions(self):
if self.action == "create":
self.permission_classes = [permissions.IsAuthenticated]
elif self.action == "list":
pass
return super(PostViewSet, self).get_permissions()
For your filterset, you need to append the lookup, so:
http://localhost:8000/posts/?content__icontains=chetorz
You can however work by default with an icontains lookup, by specifying the field manually, like:
class PostFilter(filter.FilterSet):
title = django_filters.CharFilter(lookup_expr='icontains')
content = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Post
fields = ['title', 'content']
I have these serializers
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
'id',
'name'
]
class PetSerializer(serializers.ModelSerializer):
owner = OwnerSerializer(many=False, read_only=False)
class Meta:
model = Pet
fields = [
'id',
'name',
'owner'
]
def create(self, validated_data):
"""
Create and return a new `Pet` instance, given the validated data.
"""
owner_data = validated_data.pop('owner', None)
if owner_data:
owner = User.objects.get(**owner_data)
validated_data['owner'] = owner
return Pet.objects.create(**validated_data)
This is my ViewSet:
class PetViewSet(viewsets.ModelViewSet):
"""
ModelViewSet for model pets
"""
queryset = Pet.objects.all()
serializer_class = PetSerializer
def create(self, request, *args, **kwargs):
request.data['owner'] = {'id': request.user.id, 'name': request.user.name}
pet_serializer = self.get_serializer(data=request.data)
pet_serializer.is_valid(raise_exception=True)
self.perform_create(pet_serializer)
headers = self.get_success_headers(pet_serializer.data)
return Response(pet_serializer.data, status=status.HTTP_201_CREATED, headers=headers)
But when storing it, all the data is saved, but in the created function the owner object does not arrive, it is shown as 'owner': OrderedDict ()}
So when I return the saved object, I get it with the owner with the default id, although the user I get in the view is 'owner': {'id': 10, 'name': 'My name'}} ):
My Json Response:
{
"id": 26,
"name": "My pet",
"owner": {
"id": 1
}
}
What am I doing wrong? Thank you
It looks to me like it's returning normalized data of the owner object, rather than the relation.
Instead of owner = OwnerSerializer() try something like:
owner = PrimaryKeyRelatedField(queryset=Owner.objects.all())
I made the next changes in my Serializers,the main changes was in the OwnerSerializer, 'cause the id was read only for default.
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
'id',
'name'
]
extra_kwargs = {
'id': {
'read_only': False,
'required': True
}
}
class PetSerializer(serializers.ModelSerializer):
owner = OwnerSerializer(many=False, read_only=False)
class Meta:
model = Pet
fields = [
'id',
'name',
'owner'
]
def create(self, validated_data):
"""
Create and return a new `Pet` instance, given the validated data.
"""
owner_data = validated_data.pop('owner', None)
if owner_data:
owner = User.objects.get(**owner_data)
validated_data['owner'] = owner
return Pet.objects.create(**validated_data)
.get returns one item and if it matched more, It raises an expception.
owner = User.objects.get(**owner_data), remove the trailing [0]
EDIT: Since this was not your problem, I'll tell you the problem
You're overriding create which have the validated_data, You assumed that the owner is validated but it may wrong.
Do the same logic in a method named validate_owner(self, value) in which value is the value you passed for owner in the POST before it gets validated.
Lets say I have these models:
class Download(MPTTTimeStampedModel):
endpoint = models.ForeignKey(EndPoint, related_name="downloads",)
class EndPoint(TimeStampedModel):
name = models.CharField(max_length=100, verbose_name=_(u"Nombre"))
url = models.CharField(max_length=2000, verbose_name=_(u"Url"))
These serializers:
class DownloadSerializer(serializers.ModelSerializer):
class Meta:
model = Download
fields = ('id', 'endpoint')
def create(self, validated_data):
...
def update(self, validated_data):
...
class EndPointSerializer(serializers.ModelSerializer):
class Meta:
model = EndPoint
fields = ('id', 'name', 'url')
def create(self, validated_data):
...
def update(self, validated_data):
...
And this generic api view:
class DownloadList(generics.ListCreateAPIView):
queryset = Download.objects.all()
serializer_class = DownloadSerializer
This will allow me to create a download by sending a json representation looking like this:
{
'id': null,
'endpoint': 19
}
And upon creation, the web service will send me back the data with the id from the database. Now, I actually want the web service to send me back not just the endpoint id but a complete representation of the object, Something like this:
{
'id': null,
'endpoint': {
'id': 19,
'name': 'my endpoint',
'url': 'http://www.my-endpoint.com/'
}
}
I would manage this with this serializer:
class DownloadDetailedSerializer(DownloadSerializer):
endpoint = EndPointSerializer(many = False, read_only=False)
And now the actual question: how do i tell my generic view to use this last serializer for the returned data while keeping the original DownloadSerializer for the input?
EDIT: as #neverwalkeralone suggested the way to go is creating a custom field and overriding to_representation method. But that lead me to an exception in the line serializer = EndPointSerializer(value), and after some testing I found out that it would be better to inherit my custom field from RelatedField. That implies overriding to_internal_value too. So here is what finally got the job done:
class EndPointField(serializers.RelatedField):
def to_representation(self, value):
serializer = EndPointSerializer(value)
return serializer.data
def to_internal_value(self, instance):
endpoint = EndPoint.objects.get(pk=instance)
return endpoint
def get_queryset(self):
return EndPoint.objects.all()
You can define custom field, use to_representation method to customize output format:
class EndPointField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
serializer = EndPointSerializer(value)
return serializer.data
def get_queryset(self):
return models.EndPoint.objects.all()
And use it in DownloadSerializer for endpoint field:
class DownloadSerializer(serializers.ModelSerializer):
endpoint = EndPointField()
class Meta:
model = Download
fields = ('id', 'endpoint')
UPD
Based on Kilian Perdomo Curbelo feedback EndPointField's to_representation value should be replaced with endpoint instance:
def to_representation(self, value):
endpoint = EndPoint.objects.get(pk=value.pk)
serializer = EndPointSerializer(endpoint)
return serializer.data
I try to send the following data to my django application:
{
"hashtags": ["hashtag"],
"title": "title",
"message": "message"
}
and i get this response:
{
"hashtags": [
{
"non_field_errors": [
"Invalid data. Expected a dictionary, but got int."
]
}
]
}
I have the following view defined in views.py
class PostList(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = IsAuthorizedOwnerOrReadOnly,
the models are defined like this:
class Post(models.Model):
ambassador = models.OneToOneField("User")
publish_date = models.DateTimeField(auto_now_add=True, null=True, blank=True)
hashtags = models.ManyToManyField("PostHashtags", related_query_name="posts")
title = models.CharField(max_length=TEXT_SHORT, null=True, blank=True)
message = models.CharField(max_length=TEXT_MIDDLE, null=True, blank=True)
class PostHashtags(models.Model):
hashtag = models.CharField(max_length=TEXT_SHORT, null=False)
def __unicode__(self):
return self.hashtag
and i define the serializers like this:
class PostHashtagSerializer(serializers.ModelSerializer):
class Meta:
model = PostHashtags
fields = ("hashtag",)
class PostSerializer(serializers.ModelSerializer):
hashtags = PostHashtagSerializer(many=True)
class Meta:
model = Post
fields = ("id", "hashtags", "title", "message",)
read_only_fields = ("id", 'account_name',)
It seems like the hashtags are not created automatically using my current serialisation config. Is there a way to have my hashtags created if they do not yet exist, and if they do exist, have the Post use that same hashtag? In that case, how should my serialisers be defined?
EDIT 1:
After GwynBleidD's suggestions I now get the following error:
The `.create()` method does not support writable nestedfields by default.
Write an explicit `.create()` method for serializer PostSerializer , or set `read_only=True` on nested serializer fields.
Does anyone have a suggestion for such a create method?
EDIT 2: solved it using the following serialisers
class PostHashtagSerializer(serializers.ModelSerializer):
hashtag = serializers.CharField()
class Meta:
model = PostHashtags
fields = ("hashtag",)
class PostSerializer(serializers.ModelSerializer):
hashtags = PostHashtagSerializer(many=True,)
class Meta:
model = Post
fields = ("ambassador", "hashtags", "title", "message",)
def create(self, validated_data):
hashtags_data = validated_data.pop('hashtags')
post = Post.objects.create(**validated_data)
for hashtag in hashtags_data:
ht = PostHashtags.objects.create()
ht.hashtag = hashtag.get("hashtag")
ht.save()
post.hashtags.add(ht)
post.save()
return post
Hashtags are not string, but dict in that example. You have to submit:
{
"hashtags": [{"hashtag": "hashtag"}],
"title": "title",
"message": "message"
}