I'm trying while creating object A, create object B, which are in many-to-many relation. I've read about it for some time already and I've created custom create() method in serializer to create associated B objects while creating A object, but there's no trace of B content in validated_data.
My models:
class A(models.Model):
name = models.CharField('Name', max_length=250)
description = models.TextField('Content')
timestamp = models.DateTimeField('Timestamp', auto_now_add=True)
b_field = models.ManyToManyField(B, blank=True, null=True, verbose_name='b', related_name='a')
class B(models.Model):
name = models.CharField('Name', max_length=250)
description = models.TextField('Description')
timestamp = models.DateTimeField('Timestamp', auto_now_add=True)
And A serializer:
class ASerializer(serializers.ModelSerializer):
b = BSerializer(many=True, required=False)
class Meta:
model = A
fields = '__all__'
read_only_fields = ['pk', 'timestamp']
def create(self, validated_data):
bs = validated_data.pop('b')
a = A.objects.create(**validated_data)
for b in bs:
B.objects.create(**b)
return a
I've been printing content of validated_data at the begginging of create() method and it looked like this:
{'name': 'test', 'description': 'none'}
So, no trace of B content. I've been testing with postman, sending something like this:
{
"name": "test",
"description": "none",
"b": [
{
"id": 1,
"name": "b test",
"description": "none b",
}
]
}
Okay, so I managed to solve it by myself (almost). drf-writtable-nested to the rescue.
First of, I changed my A serializer so it now inherits after WritableNestedModelSerializer from package mentioned above. Thanks to this, content of b is now visible in validated_data passed to create() method. Speaking of which, from what I have read (and what can be seen in the question itself), there's a need to create m2m objects in overwritten create() method. Mine looks like this now:
def create(self, validated_data):
bs_data = validated_data.pop('b')
a = self.Meta.model.objects.create(**validated_data)
for b_data in bs_data:
b = B.objects.create(**b_data)
a.b.add(b)
return a
There's still place to validate B instance in this serializer, but let's say, this solves my issue.
Related
TDLR : what is the best way to implement tags in django-rest-framework. where the tags has a created_by field which is the currently authenticated user.
I am trying to achieve a very simple/common thing, add tags to posts. But apparently its not a piece of cake.
So i have a posts model and a tags models (may to many relation). I want the user to be able to update and create the posts. when creating or updating posts he should be able to update the tags of the posts. When a post is tagged with a new tag, that tag should be created if it dosent exist. Also i want to user to be able to specify the tags as a list of strings in the request.
Example request
{
"name": "testpost1",
"caption": "test caption",
"tags": ["tag1", "tag2"],
},
models.py
class Tags(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
name = models.CharField(max_length=50, unique=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="created_tags")
class Posts(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
name = models.CharField(max_length=50)
caption = models.TextField(max_length=1000)
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
tags = models.ManyToManyField('Tags', related_name='posts')
serializers.py
class TagsSerializerMini(serializers.ModelSerializer):
created_by = serializers.PrimaryKeyRelatedField(default=serializers.CurrentUserDefault(), queryset=User.objects.all())
class Meta:
model = Tags
fields = ('name', 'created_by')
extra_kwargs = {
'created_by': {'write_only': True},
'name': {'validators': []},
}
def create(self, validated_data):
tag, created = Tags.objects.get_or_create(**validated_data)
if not created:
raise exceptions.ValidationError(validated_data['name']+" already exists.")
return tag
def to_representation(self, instance):
ret = super(TagsSerializerMini, self).to_representation(instance)
data = dict()
data['name'] = ret['name']
return data
I have tried two methods. Using nested serializer and using slug related field.
When using SlugRealtedfield, it throws as validation error that the tag object dosent exists. I was planning if i could deisable this check, i could create all tags before create() and call super create. But i could'nt bypass that validation check. Also i couldnt figure out how to pass the current user to the slugrelatedfield.
After some searching, i planned to use nested serializers. But i have to specify the tags as dict [{"name":"tag1"}]. Also i have to define custom create and update. I could get the create to work, but not the update.
class PostsSerializer(QueryFieldsMixin, WritableNestedModelSerializer):
created_by = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault())
class Meta:
model = Posts
fields = ('id', 'name', 'caption', 'tags', 'created_by')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tags'] = TagsSerializerMini(many=True, required=False, context=self.context)
def create(self, validated_data):
tags_data = validated_data.pop('tags', [])
post = Posts.objects.create(**validated_data)
for tag in tags_data:
t, _ = Tags.objects.get_or_create(name=tag["name"])
post.tags.add(t)
return post
In my opinion, it is more elegant to use SlugRelatedField and not a nested serializer, because this way you will have an array of tags (and an array of tag names in the response) instead of an array of dictionaries [{ "name": "tag name" }]
As you mentioned, the validation check fails if the tag doesn't exist.
I managed to overcome this by subclassing SlugRelatedField and overriding "to_internal_value" method. In the original implementation this method tries to get an object from the queryset, and if an object doesn't exist it fails the validation. So instead of calling "get" method, I'm calling "get_or_create":
class CustomSlugRelatedField(serializers.SlugRelatedField):
def to_internal_value(self, data):
try:
obj, created = self.get_queryset().get_or_create(**{self.slug_field: data})
return obj
except (TypeError, ValueError):
self.fail('invalid')
If you can accept using two fields, here is my solution:
Use a SlugRelatedField for read only, and a ListField for write only, so you can have list of strings rather than dictionaries.
To get current user, you can use self.context['request'].user in serializer functions.
Below is sample code(not tested):
class PostsSerializer(serializers.ModelSerializer):
tags = serializers.SlugRelatedField(many=True, slug_field='name', read_only=True)
update_tags = serializers.ListField(
child=serializers.CharField(max_length=30), write_only=True)
class Meta:
model = Posts
exclude = ()
def create(self, validated_data):
tag_names = validated_data.pop('update_tags')
instance = super().create(validated_data)
user = self.context['request'].user
tags = []
for name in tag_names:
tag, created = Tags.objects.get_or_create(name=name, defaults={'created_by': user})
tags.append(tag)
instance.tags.set(tags)
return instance
def update(self, instance, validated_data):
tag_names = validated_data.pop('update_tags')
instance = super().update(instance, validated_data)
user = self.context['request'].user
tags = []
for name in tag_names:
tag, created = Tags.objects.get_or_create(name=name, defaults={'created_by': user})
tags.append(tag)
instance.tags.set(tags)
return instance
note: I use instance.tags.set rather than instance.tags.add, so that tag relations can be deleted. You just need to always send all tags though.
I am supposed to override the create() method on my serializer but I cannot understand why DRF cannot get me the real value of a validate_data.get('some_field', None).
Here is my models.
class Heat(models.Model):
# Fields
performer = models.CharField(max_length=25)
is_bred = models.BooleanField(default=False)
note = models.TextField(max_length=250, blank=True, null=True)
class Breeding(models.Model):
# Relationship Fields
heat = models.OneToOneField(
Heat,
on_delete=models.CASCADE)
# Fields
performer = models.CharField(max_length=25)
remarks = models.TextField(max_length=255, blank=True, null=True)
And here is my serializer class
class BreedingSerializer(serializers.ModelSerializer):
def create(self, validated_data):
heat_id = validated_data.get('heat', None)
heat = Heat.objects.get(pk=heat_id)
breeding = Breeding.objects.create(**validated_data)
return breeding
class Meta:
model = Breeding
fields = ['id', 'heat', 'performer', 'remarks']
Here is my validated_data looks like.
{
"heat": 1,
"performer": "Some person",
"remarks": "testing"
}
When i try to print the heat_id it yield the value as 'Heat Object' not the actual data provided as heat integer 1. but it works well in this code breeding = Breeding.objects.create(**validated_data).
Can anyone explain?
I honestly doubt that the dict you printed is actually validated_data. It looks more like the raw data that was sent to the serializer.
Since heat is a relationship, DRF's ModelSerializer automatically instantiates the object with PK = 1. Also, that is why Breeding.objects.create(**validated_data) works. If you tried something like Breeding.objects.create(heat=1), Django would probably complain because it doesn't expect the heat keyword to be a PK, but the object instead.
In my app which use DRF, I want to use model serializer with multiple related objects.
models.py:
class JobType(models.Model):
name = models.CharField(null=False, max_length=250)
class Offer(models.Model):
user = models.ForeignKey(User, null=False)
job_type = models.ForeignKey(JobType, null=False)
salary = models.DecimalField(null=False, max_digits=8,
decimal_places=2)
serializers.py:
class JobTypeSerializer(serializers.ModelSerializer):
class Meta:
model = JobType
fields = ('id', 'name')
class OfferSerializer(serializers.ModelSerializer):
job_type = JobTypeSerializer()
class Meta:
model = Offer
fields = (
'salary', 'job_type', 'user'
)
views.py:
class SalaryViewSet(viewsets.ModelViewSet):
queryset = Salary.objects.all()
serializer_class = SalaryFullSerializer
What I want to achieve:
when I do GET request on my api/offers I want to have something like:
[
{
"salary": 1000,
"user: 1,
"job_type": {
"id": 1,
"name": "Developer",
}
}
]
so, basically, when GET offers is made, I want to have nested related object with all it's properties.
On other hand, when POST offers is made, I want to limit JobType choices.
When I've removed job_type = JobTypeSerializer() from OfferSerializer I had nice dropdown with available choices (in DRF debug). But it caused that GET on offers returned only JobOffer's ID in results.
How can I achieve desired behavior?
You can use different serializer for POST and GET requests.
Override get_serializer_class
def get_serializer_class(self):
if self.request.method == 'POST':
return SalaryPostSerializer
return SalaryFullSerializer
I wanted to have a serializer which would read as an object of fields on a related models, but save as a pk value to the related object. Something like a nested model with depth=1, but with more flexibility on the nested model. Essentialy I wanted the following query structure
GET child/{id}
{
"id": 1,
"name": "child",
"parent": {
"id": 1,
"name": "parent"
}
}
POST child/{id}
{
"id": 1,
"name": "child",
"parent": 1
}
So I wrote the following serializers:
# models.py
class Parent(models.Model):
name = models.CharField(max_length=255)
class Child(models.Model):
name = models.CharField(max_length=255)
parent = models.ForeignKey(Parent)
# serializers.py
class ParentSerializer(serializers.ModelSerializer):
"""Write as pk, read as object"""
class Meta:
model = models.Parent
fields = ('id', 'name')
def to_internal_value(self, data):
return self.Meta.model.objects.get(pk=data)
class ChildSerializer(serializers.ModelSerializer):
parent = ParentSerializer()
class Meta:
model = models.Child
fields = ('id', 'name', 'parent')
This almost works as expected, but the child serializer doesn't work when passed a querydict. To illustrate the problem:
# This works great! everything as expected
parent = Parent.objects.create(name='parent')
data = {'name': 'child', 'parent': parent.pk}
serializer = ChildSerializer(None, data=data)
serializer.is_valid()
serializer.save()
# This borks
data = QueryDict('name={0}&parent={1}'.format('child', parent.pk))
serializer = ChildSerializer(None, data=data)
serializer.is_valid()
serializer.save()
TypeError: int() argument must be a string, a bytes-like object or a number, not 'dict'
As a workaround I've forced ChildSerializer to convert QueryDicts to normal dictionaries, but I'd still like to know why the above fails and if there is a better option for this kind of API structure.
I'm attempting to build a nested relationship using Django Rest Framework 3.0. I've created my serializers and have attempted to override the create() function. My models are defined as follows:
class Item(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
name = models.CharField(max_length=200)
description = models.CharField(max_length=1000)
categories = models.ManyToManyField(Category, null=True, blank=True)
class Price(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
item = models.ForeignKey(Item, related_name='prices')
name = models.CharField(max_length=100)
cost = models.FloatField()
As you'll note, I can have multiple prices for my items. My serializers are defined as follows:
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
owner = serializers.Field(source='owner.username')
exclude = ('user',)
class ItemSerializer(serializers.ModelSerializer):
prices = PriceSerializer(many=True, required=False)
categories = CategorySerializer(many=True, required=False)
class Meta:
model = Item
owner = serializers.Field(source='owner.username')
fields = ('id', 'name', 'description', 'prices', 'categories')
def create(self, validated_data):
user = validated_data.get('user')
# Get our categories
category_data = validated_data.pop('categories')
# Create our item
item = Item.objects.create(**validated_data)
# Process the categories. We create any new categories, or return the ID of existing
# categories.
for category in category_data:
category['name'] = category['name'].title()
category, created = Category.objects.get_or_create(user=user, **category)
item.categories.add(category.id)
item.save()
return item
When I try and POST a new item:
{
"name": "Testing",
"description": "This is a test",
"categories": [
{
"name": "foo"
},
{
"name": "bar"
}
],
"prices": [
{
"name": "Red",
"cost": 10
}
]
}
I get the following error:
{
"prices": [
{
"item": [
"This field is required."
]
}
]
}
Presumably because the Price serializer has no idea what the ID of the new item is. I've tried overriding this functionality in the create() function of my serializer, but it appears as though the serializer's validation is being hit before I have the opportunity to create the item and associate it with the price.
So - How do I create a new item, get the item ID, and then create each of the new prices?
The problem is that your PriceSerializer is looking for the item key because it is specified on the Price model. This isn't immediately obvious because you are using Meta.exclude instead of Meta.fields.
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
exclude = ('user',)
Is the same as writing
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
fields = ('id', 'item', 'name', 'cost', )
Which makes it very clear what your issue is. Because your item field on the model does not have empty=True (or null=True) set, Django REST Framework automatically generates it as a PrimaryKeyRelatedField with required=True. This is why you are getting the This field is required is required error, because Django REST Framework cannot automatically detect that it is coming from a parent serializer which already has that field.
You can get around this by removing the field from the serializer, as it doesn't appear to ever be needed.
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
fields = ('id', 'name', 'cost', )
This will no longer display the item field though, but I suspect this isn't actually an issue for you.