Mark Seliaev
Mark Seliaev

Reputation: 570

Automatically create Tag objects when creating an object, Django REST Framework

I have the Task model:

class Task(models.Model):
    name = models.CharField(max_length=200, blank=True)
    description = models.TextField(max_length=1000, blank=True)
    completed = models.BooleanField(default=False)
    date_created = models.DateField(auto_now_add=True)
    due_date = models.DateField(null=True, blank=True)
    date_modified = models.DateField(auto_now=True)
    tasklist = models.ForeignKey(Tasklist, null=True, related_name='tasks', on_delete=models.CASCADE)
    tags = models.ManyToManyField(TaskType, related_name='tasks')

And class TaskType (tag in other words):

class TaskType(models.Model):
    name = models.CharField(max_length=200)

I also have TaskSerializer:

class TaskSerializer(serializers.ModelSerializer):
    tags = serializers.SlugRelatedField(many=True, slug_field='name', queryset=TaskType.objects.all())

    class Meta:
        model = Task
        fields = '__all__'
        read_only_fields = ('date_created', 'date_modified', 'tasklist')

When I create a Task, to add some tags I need to create them in appropriate view firstly, but I want them to be created on the fly. So in case of editing the task, I added update method:

def update(self, request, *args, **kwargs):
    instance = self.get_object()
    tag_names = request.data.get('tags', [])
    for tag_name in tag_names:
        tag, created = TaskType.objects.get_or_create(name=tag_name)
        instance.tags.add(tag)

    serializer = self.serializer_class(instance=instance, data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save()
    return Response(serializer.data)

It works fine, but when I add new tags when creating a new task it fails (400 bad request):

{
    "tags": [
        "Object with name=%new_tag% does not exist."
    ]
}

I figured out that it would be a good way to create appropriate tag object before creating Task with it, so I added perform_create method:

def perform_create(self, serializer):
    print('debug')
    tag_names = self.request.data.get('tags', [])
    for tag_name in tag_names:
        tag, created = TaskType.objects.get_or_create(name=tag_name)

    list_id = self.kwargs.get('list_id', None)
    try:
        tasklist = Tasklist.objects.get(pk=list_id)
    except Tasklist.DoesNotExist:
        raise NotFound()
    serializer.save(tasklist=tasklist)

It doesn't help me, actually I am not sure if perform_create method at least is called, because I see no print('debug') in the console (when I create Task with an existing tag I see it). So the question is how to change perform_create method to be able to create fresh Tasks without creating Tag firstly.

Upvotes: 1

Views: 1256

Answers (2)

Igonato
Igonato

Reputation: 10806

First, you don't need to manually add tags in your view methods. The serializer will do it for you.

Second, the update method is used when you update your model. When creating you need to override create method, perform_create works, but happens too late:

def create(self, request, *args, **kwargs):
    tag_names = request.data.get('tags', [])
    for tag_name in tag_names:
        TaskType.objects.get_or_create(name=tag_name)

    serializer = self.serializer_class(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save()
    return Response(serializer.data)

Third, once you've created new tags, call super().*method* and let the framework do the work for you:

def update(self, request, *args, **kwargs):
    tag_names = request.data.get('tags', [])
    for tag_name in tag_names:
        TaskType.objects.get_or_create(name=tag_name)

    return super().update(request, *args, **kwargs)

def create(self, request, *args, **kwargs):
    tag_names = request.data.get('tags', [])
    for tag_name in tag_names:
        TaskType.objects.get_or_create(name=tag_name)

    return super().create(request, *args, **kwargs)

There is also partial_update method you need to add if you are planning to use PATCH method, so you might be better off overriding your serializer's to_internal_value method instead of each of those:

class TaskSerializer(serializers.ModelSerializer):
    tags = serializers.SlugRelatedField(
        many=True, slug_field='name', queryset=TaskType.objects.all())

    class Meta:
        model = Task
        fields = '__all__'
        read_only_fields = ('date_created', 'date_modified', 'tasklist')

    def to_internal_value(self, data):
        for tag_name in data.get('tags', []):
            TaskType.objects.get_or_create(name=tag_name)
        return super().to_internal_value(data)

Upvotes: 1

zaidfazil
zaidfazil

Reputation: 9245

Try editing your view like this,

def update(self, request, *args, **kwargs):
    instance = self.get_object()
    tag_names = request.data.get('tags', [])
    serializer = self.serializer_class(instance=instance, data=request.data)
    if serializer.is_valid(raise_exception=True)
        new_object = serializer.save()
    if new_object:
        for tag_name in tag_names:
            tag, created = TaskType.objects.get_or_create(name=tag_name)
            new_object.tags.add(tag)
    return Response(serializer.data)

Upvotes: 0

Related Questions