Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing Tags using Django rest framework

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
like image 530
Nithin Avatar asked May 06 '18 18:05

Nithin


People also ask

Is Django GOOD FOR REST API?

Django REST framework (DRF) is a powerful and flexible toolkit for building Web APIs. Its main benefit is that it makes serialization much easier. Django REST framework is based on Django's class-based views, so it's an excellent option if you're familiar with Django.

How use Django REST framework template?

Using template packsThe render_form tag takes an optional template_pack argument, that specifies which template directory should be used for rendering the form and form fields. REST framework includes three built-in template packs, all based on Bootstrap 3. The built-in styles are horizontal , vertical , and inline .


1 Answers

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')
like image 126
Vitali Kaspler Avatar answered Sep 23 '22 04:09

Vitali Kaspler