Reputation: 5840
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
Upvotes: 6
Views: 4426
Reputation: 91
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.
Upvotes: 0
Reputation: 1420
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')
Upvotes: 2