[Fixed]-Using Django-taggit with django-rest-framework, i'm not able to save my tags

17šŸ‘

meet the same question. But I just want to save the tag list directly by TaggableManager (without TagListSerializer and TagsListAPIView). My solution is:

class MyModel(models.Model):
    ...
    tags = TaggableManager(blank=True)

    def get_tags_display(self):
        return self.tags.values_list('name', flat=True)

class MyModelSerializer(serializers.HyperlinkedModelSerializer):
    ...
    tags = serializers.Field(source='get_tags_display') # more about: http://www.django-rest-framework.org/api-guide/fields#generic-fields
    ...

class MyModelViewSet(viewsets.ModelViewSet):
    ...
    def post_save(self, *args, **kwargs):
        if 'tags' in self.request.DATA:
            self.object.tags.set(*self.request.DATA['tags']) # type(self.object.tags) == <taggit.managers._TaggableManager>
        return super(MyModelViewSet, self).post_save(*args, **kwargs)

The post data of tags data will be [ā€˜tagA’, ā€˜tagB’,…], the TaggableManager will handle it. Thx.

For DRF>3.1, you just need to override create and update in your ModelSerializer class:

class StringListField(serializers.ListField): # get from http://www.django-rest-framework.org/api-guide/fields/#listfield
    child = serializers.CharField()

    def to_representation(self, data):
        return ' '.join(data.values_list('name', flat=True)) # you change the representation style here.


class MyModelSerializer(serializers.ModelSerializer):
    tags = StringListField()

    class Meta:
        model = models.MyModel

    def create(self, validated_data):
        tags = validated_data.pop('tags')
        instance = super(MyModelSerializer, self).create(validated_data)
        instance.tags.set(*tags)
        return instance

    def update(self, instance, validated_data):
        # looks same as create method
šŸ‘¤gzerone

7šŸ‘

I used to follow the following ways to serialize taggit objects but currently django-taggit provide a built in serializer https://github.com/jazzband/django-taggit/blob/master/taggit/serializers.py and it was vendor from the package I mentioned previously.

"""
Django-taggit serializer support
Originally vendored from https://github.com/glemmaPaul/django-taggit-serializer
"""
import json

# Third party
from django.utils.translation import gettext_lazy
from rest_framework import serializers


class TagList(list):
    def __init__(self, *args, **kwargs):
        pretty_print = kwargs.pop("pretty_print", True)
        super().__init__(*args, **kwargs)
        self.pretty_print = pretty_print

    def __add__(self, rhs):
        return TagList(super().__add__(rhs))

    def __getitem__(self, item):
        result = super().__getitem__(item)
        try:
            return TagList(result)
        except TypeError:
            return result

    def __str__(self):
        if self.pretty_print:
            return json.dumps(self, sort_keys=True, indent=4, separators=(",", ": "))
        else:
            return json.dumps(self)


class TagListSerializerField(serializers.Field):
    child = serializers.CharField()
    default_error_messages = {
        "not_a_list": gettext_lazy(
            'Expected a list of items but got type "{input_type}".'
        ),
        "invalid_json": gettext_lazy(
            "Invalid json list. A tag list submitted in string"
            " form must be valid json."
        ),
        "not_a_str": gettext_lazy("All list items must be of string type."),
    }
    order_by = None

    def __init__(self, **kwargs):
        pretty_print = kwargs.pop("pretty_print", True)

        style = kwargs.pop("style", {})
        kwargs["style"] = {"base_template": "textarea.html"}
        kwargs["style"].update(style)

        super().__init__(**kwargs)

        self.pretty_print = pretty_print

    def to_internal_value(self, value):
        if isinstance(value, str):
            if not value:
                value = "[]"
            try:
                value = json.loads(value)
            except ValueError:
                self.fail("invalid_json")

        if not isinstance(value, list):
            self.fail("not_a_list", input_type=type(value).__name__)

        for s in value:
            if not isinstance(s, str):
                self.fail("not_a_str")

            self.child.run_validation(s)

        return value

    def to_representation(self, value):
        if not isinstance(value, TagList):
            if not isinstance(value, list):
                if self.order_by:
                    tags = value.all().order_by(*self.order_by)
                else:
                    tags = value.all()
                value = [tag.name for tag in tags]
            value = TagList(value, pretty_print=self.pretty_print)

        return value


class TaggitSerializer(serializers.Serializer):
    def create(self, validated_data):
        to_be_tagged, validated_data = self._pop_tags(validated_data)

        tag_object = super().create(validated_data)

        return self._save_tags(tag_object, to_be_tagged)

    def update(self, instance, validated_data):
        to_be_tagged, validated_data = self._pop_tags(validated_data)

        tag_object = super().update(instance, validated_data)

        return self._save_tags(tag_object, to_be_tagged)

    def _save_tags(self, tag_object, tags):
        for key in tags.keys():
            tag_values = tags.get(key)
            getattr(tag_object, key).set(tag_values)

        return tag_object

    def _pop_tags(self, validated_data):
        to_be_tagged = {}

        for key in self.fields.keys():
            field = self.fields[key]
            if isinstance(field, TagListSerializerField):
                if key in validated_data:
                    to_be_tagged[key] = validated_data.pop(key)

        return (to_be_tagged, validated_data)

http://blog.pedesen.de/2013/07/06/Using-django-rest-framework-with-tagged-items-django-taggit/

With the release of the Django Rest Framework 3.0, the code for the TagListSerializer has changed slightly. The serializers.WritableField was depreciated in favour for serializers.Field for the creation of custom serializer fields such as this. Below is the corrected code for Django Rest Framework 3.0.

class TagListSerializer(serializers.Field):
    def to_internal_value(self, data):
        if type(data) is not list:
            raise ParseError("expected a list of data")
        return data

    def to_representation(self, obj):
        if type(obj) is not list:
            return [tag.name for tag in obj.all()]
        return obj

I now use the bulit in taggit serializer which was taken from https://github.com/glemmaPaul/django-taggit-serializer library.

šŸ‘¤auvipy

0šŸ‘

I had a bunch of errors but i found a way to resolve my problem. Maybe not the best as i’m pretty new with all of this but for now it works.

I’ll try to describe all my errors maybe it’ll help someone.

First my angularjs send a json which match exatly the queryset

So for example with my model events below, angularjs send to the API:

angularjs controller

Now let’s begin with all my errors:

  • ā€œA tag with this name already existā€

When i re-use a tag i have this error. Don’t know why because with a classic validation without the API, all is working fine.

  • With a new tag nothing is saved too.

When i try to use a new tag on my event event model nothing is saved on the database. Angularjs received a response with the tag name but with an id of null (see the pitcure on my original question)

  • ā€œAttributeError: ā€˜RelationsList’ object has no attribute ā€˜add'ā€

Now i’m trying to think that to register my tags i need to have an instance of event already created. Thanks to that i will be able to add my tag on it like it’s describe in the doc.

apple.tags.add(ā€œredā€, ā€œgreenā€, ā€œfruitā€)

So i decided to add a post_save in my events_views.py:

class EventListAPIView(generics.ListCreateAPIView):
    queryset = Event.objects.all()
    model = Event
    serializer_class = EventSerializer
    paginate_by = 100

    def pre_save(self, obj):
        """
        Set the object's owner, based on the incoming request.
        """
        obj.user = self.request.user
        return super(EventListAPIView, self).pre_save(obj)

    def post_save(self, obj, created=False):
        print 'tags', self.request.DATA
        obj.tags.add(self.request.DATA['tags'])
        return super(EventListAPIView, self).post_save(obj)

But now as is said i have this error AttributeError: ā€˜RelationsList’ object has no attribute ā€˜add’.
Actually, it’s obvious since obj.tags is a list of object and not the TaggableManager anymore.

So i decided to start over and send my tags not in ā€˜tags’ but in another custom property ā€˜tagged’ to avoid conflit with the TaggableManager.

  • ā€œTypeError: unhashable type: ā€˜list'ā€

New error šŸ™‚ I found the solution with this django-taggit-unhashable-type-list

def post_save(self, obj, created=False):
    map(obj.tags.add, self.request.DATA['tagged'])
    return super(EventListAPIView, self).post_save(obj)
  • ā€œTypeError: unhashable type: ā€˜dict'ā€

Now, i figured it out that the tags i sent are not well formatted. I changed it (on the angularjs side) to send an array like this [ā€˜jazz’,’rock’] instead of [object, object]. Stupid mistake from a beginner.

Now the magic happen, response received by angularjs is good:

angular-response-ok

Sorry for my english. I know it may not be the best solution and i will try to update it when i’ll find another solution.

šŸ‘¤Epok

Leave a comment