I've got a problem with DRF serialization. My main goal is to create an instance which has a related field but instead of providing the related models id i want to use it's other unique field. At the same time when I will serialize my model to present it (not create) i would like to get the default related fields value. Here's an example
class Comment(models.Model):
description = models.TextField()
creator = models.ForeignKey(User, ...)
x = Creator.objects.get(pk=1)
print(x.unique_field)
> 'some data'
client.post('comment-detail', data={
'description': 'some description',
'creator_unique_field': 'some data'
})
# this should create a comment while using some unique creators field
# which is not pk or id
print(client.get('comment-detail', data={'pk':1}))
{
'description' 'some description',
'creator': 1,
}
I don't know If i should change models Serializer of ViewSet's create() and retrieve(). Im starting with DRF and can't get my head around it
Overriding the Serializer create method is a good place for this. one can query for the unique_field user there.
class CommentView(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(creator=self.request.user)
class CommentSerializer(serializers.Serializer):
creator_unique_field = serializer.SomeField()
def create(self, validated_data):
creator = Creator.objects.get(unique_field=validated_data['creator_unique_field'])
comment, created = Comment.objects.get_or_create(creator=creator, **validated_data)
return comment
class Meta:
model = Comment
fields = '__all__'
Related
DRF docs says that "By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create create() and/or update() methods in order to explicitly specify how the child relationships should be save."<<
In general this is true except for one case. When User class is a nested object in serializer.
Use case is to add existing user to newly created organization.
Consider my simple example where I noticed that issue:
views.py
class UserOrganizationViewSet(viewsets.ModelViewSet):
# authentication_classes = (JwtAuthentication, SessionAuthentication)
# permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,)
serializer_class = UserOrganizationSerializer
queryset = UserOrganization.objects.all()
models.py:
class UserOrganization(models.Model):
name = models.CharField(max_length=256)
users = models.ManyToManyField(User, blank=True, related_name="user_organizations", through='OrganizationMember')
def __str__(self):
return self.name
class OrganizationMember(models.Model):
organization = models.ForeignKey(UserOrganization, on_delete=CASCADE)
user = models.ForeignKey(User, on_delete=CASCADE)
joined = AutoCreatedField(_('joined'))
serializers.py:
First version - User class has its own simple serializer and it is used as nested serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ['username']
class UserOrganizationSerializer(serializers.ModelSerializer):
users = UserSerializer(many=True, required=False)
class Meta:
model = UserOrganization
fields = ['id', 'name', 'users']
read_only_fields = ['id']
Is this case I am able to fetch data via GET method (Organization created through admin panel):
But when I am trying to create organization I am getting error which says that user already exist:
So, I checked what will happen when I try to paste not existed username in JSON data send. According to expectation DRF says that I need to override create() method:
{
"name": "Test2",
"users": [
{
"username": "admins"
}
]
}
AssertionError: The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `organization.serializers.UserOrganizationSerializer`, or set `read_only=True` on nested serializer fields.
And there is a first weird behavior. DRF still tries to created User object:
My create method looks like this:
def create(self, validated_data):
print("validated_data", validated_data)
users = validated_data.pop('users', [])
user_organization = UserOrganization.objects.create(**validated_data)
print("users", users)
for user in users:
userSerializer = UserSerializer(user)
print("userSerializer.data", userSerializer.data)
username = userSerializer.data['username']
print("username", username)
user_organization.users.add(get_user_model().objects.get(username=username))
return user_organization
In this case my method isn't called at all.
In case when I passed user which does not exist, my create() method is called and exeption is thrown as it should be. (I know I am not handling errors, this is just sandbox):
{
"name": "Test2",
"users": [
{
"username": "admin1"
}
]
}
django.contrib.auth.models.User.DoesNotExist: User matching query does not exist.
New model class UserProfile and serializer for this class used as nested:
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ['name']
class UserOrganizationSerializer(serializers.ModelSerializer):
profiles = UserProfileSerializer(many=True)
class Meta:
model = UserOrganization
fields = ['id', 'name', 'profiles']
read_only_fields = ['id']
def create(self, validated_data):
print("validated_data ", validated_data)
users = validated_data.pop('profiles', [])
user_organization = UserOrganization.objects.create(**validated_data)
print("create", users)
for user in users:
print("username = ", user['name'])
user_organization.profiles.add(UserProfile.objects.get(name=user['name']))
user_organization.save()
return user_organization
Small change was needed to change field users to profiles inside UserOrganization.
Creating organization with existed user profile (created by admin) via POST:
In this scenario there is no such problem as mentioned in first case.
Final solution using SlugReleatedField works as expected. No serializer used for User.
class UserOrganizationSerializer(serializers.ModelSerializer):
users = serializers.SlugRelatedField(many=True, queryset=get_user_model().objects.all(), slug_field='username')
class Meta:
model = UserOrganization
fields = ['id', 'name', 'users']
read_only_fields = ['id']
Above code does what I exactly need and is very simple. Here is small concern regarding performance when fetched all users from DB in this line:
queryset=get_user_model().objects.all()
Can someone explain why this happens what we could observe in scenario 1?
Sorry for this post being so long. I tried to shorten it as much I as can. I can put whole project in github if it will be needed for readability.
I have an example model which has a fk relation with user model and Blog model. Now I have a get api which only requires certain fields of user to be displayed.
My model:
class Example(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
related_name="user_examples",
)
blog = models.ForeignKey(
Blog,
on_delete=models.CASCADE,
null=True,
related_name="blog_examples",
)
/................./
Now my view:
class ExampleView(viewsets.ModelViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
def list(self, request, *args, **kwargs):
id = self.kwargs.get('pk')
queryset = Example.objects.filter(blog=id)
serializer = self.serializer_class(queryset,many=True)
return Response(serializer.data,status=200)
My serializer:
class ExampleSerializer(serializers.ModelSerializer):
class Meta:
model = Example
fields = ['user','blog','status']
depth = 1
Now when I call with this get api, I get all example objects that is required but all the unnecessary fields of user like password, group etc . What I want is only user's email and full name. Same goes with blog, I only want certain fields not all of them. Now how to achieve this in a best way??
You will have to specify the required fields in nested serializers. e.g.
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = ['title', 'author']
class ExampleSerializer(serializers.ModelSerializer):
blog = BlogSerializer()
class Meta:
model = Example
fields = ['user','blog','status']
are you setting depth in serializer's init method or anywhere else? beacause ideally it should only display id's and not anything else. if yes then set depth to zero and use serializer's method field to return data that you need on frontend. I can provide you with example code samples
I have a many-to-one relationship between the following models
class Story(models.Model):
id = models.CharField(max_length=12, primary_key=True)
class Article(models.Model):
id = models.CharField(max_length=16, primary_key=True)
title = models.CharField(max_length=500)
address = models.URLField()
story = models.ForeignKey(to=Story, blank=True, null=True, on_delete=models.CASCADE)
Suppose I post several article objects to the database successfully.
I identify that the articles with the ids
['1', '2', '3']
are all reporting on a particular Story.
I want create a Story via a POST method to a CreateAPIView view like this
POST http://127.0.0.1/news/story {articles': ['1', '2', '3']}
Here is my serializer
class StorySerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
articles = serializers.PrimaryKeyRelatedField(many=True, allow_empty=False, queryset=Article.objects.all())
class Meta:
model = Story
fields = ('id', 'articles')
Here is my view
class StoryList(generics.ListCreateAPIView):
serializer_class = StorySerializer
queryset = Story.objects.all()
I want to ensure that 1) the articles exist. 2) the article story is updated before the Story object is created.
Suppose I run this as it is, I will get the following error:
Got a TypeError when calling Story.objects.create(). This may be
because you have a writable field on the serializer class that is not
a valid argument to Story.objects.create(). You may need to make the
field read-only, or override the StorySerializer.create() method to
handle this correctly.
So here is an attempt to override the create() method:
def create(self, validated_data):
story_id = None
for article_id in validated_data['articles']:
article = Article.objects.get(id=article_id)
story_id = article.story_id
if story_id:
break
story = Story.objects.get(id=story_id) if story_id else Story.objects.create()
for article_id in validated_data['articles']:
article = Article.objects.get(id=article_id)
article.story_id = story.id
article.save()
story.save()
return story
def update(self, instance, validated_data):
return self.create(validated_data)
The idea here is make sure there are no overlapping stories by merging them.
When I try POST to this view, I encounter a DoesNotExist thrown by the line Article.objects.get(id=article_id)
My questions are
1) Minor : Why am I getting this error
2) Major : Is there a cleaner / correct way of addressing such a use case in django?
Thank you
class StoryList(generics.ListCreateAPIView):
serializer_class = StorySerializer
query_set = Story.objects.all()
It's should be quertset not query_set.
1) Minor : Why am I getting this error
You gived an illegal article_id which is not exist.
2) Major : Is there a cleaner / correct way of addressing such a use case in django?
drf-writable-nested can handle nested write in drf well.
suppose this model:
class Tweek(models.Model):
content = models.CharField(max_length=140)
date = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, related_name='author')
class Meta:
ordering = ['-date']
def __unicode__(self):
return self.content
Everthing works fine, now i try to bind a rest api uppon. I've installed the django rest framework and I can retrieve tweeks but I cannot create new ones.
I have this serializer for the Tweek model:
class TweekSerializer(serializers.ModelSerializer):
author = UserSerializer()
class Meta:
model = Tweek
fields = ('content', 'date', 'author')
def create(self, validated_data):
author_data = validated_data.pop('author')
author = User.objects.get(username=author_data)
return Tweek.objects.create(author=author, **validated_data)
and the user serializer somply looks like:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', 'first_name', 'last_name')
but the post returns
{
"author": {
"username": [
"This field must be unique."
]
}
}
I've followed the doc, but i'm lost with this case :(
Any idea ?
The issue here is that any field with unique=True set, like the username field on the Django User model, will automatically have the UniqueValidator added to the serializer field. This validator is what is triggering the message you are seeing, and you can remove the validation by setting validators to [] (an empty list) when initializing the field.
The other issue that you are going to run into is that you are trying to create an object with a foreign key, but you are returning the full serialized object in your response. This issue is easier fixed by using a second field for setting the id, that is write-only, and using the original serializer field for the nested representation and making that read-only.
You can find more information in the following Stack Overflow question: DRF: Simple foreign key assignment with nested serializers?
I have written basic model serializers in Django where the api mimics the data model. I now have a requirement to store User Preference in database. The api contains an array.
My User Models :
class User(models.Model):
email_id = models.EmailField(max_length=80, blank=True, primary_key=True)
class UserPreference(models.Model)
email_id = models.ForeignKey('User')
preference = models.CharField(maxlength=20)
An ideal json post request would look something like this
{
email:"abhishek#gmail.com"
preference : [ 'books', 'food', 'lifestyle', 'travel']
}
I wish to save this json schema to the UserPreference model. This requires multiple inserts for preference. What will be a good serializer design for it ?
I tried
class UserPreferenceSerializer(serializers.ModelSerializer):
class Meta:
model = UserPreference
fields = ('email_id', 'preference')
you could use StringRelatedField of Django Rest Framework.
Make below changes and you will get the response in the way you want.
models.py (put related_name there)
class UserPreference(models.Model):
email_id = models.ForeignKey(User, related_name='preference')
preference = models.CharField(maxlength=20)
serializers.py
class UserSerializer(serializers.ModelSerializer):
preference = serializers.StringRelatedField(many=True)
class Meta:
model = User
fields = ('email_id', 'preference')
You could make your model like this:
class UserPreference(models.Model)
email_id = models.ForeignKey('User')
preference = models.ManyToManyField('Preference') #create model for preferences
Then add custom create method to your serializer:
def create(self, validated_data):
user = self.context.get('user') #you can pass context={'user': self.request.user} in your view to the serializer
up = UserPreference.objects.create(email_id=user)
up.save()
preference = validated_data.get('preference', [])
up.preference.add(*preference)
up.save()
return up