【问题标题】:Django Rest Framework - deserialization of a TaggableManager fieldDjango Rest Framework - TaggableManager 字段的反序列化
【发布时间】:2020-06-09 12:50:24
【问题描述】:

我使用django-taggit 为我的模型添加标签。 Django 版本:2.2.10,Python 版本:3.8.1

现在我正在尝试将标签与 django rest-framework 集成,例如创建/更新/删除带有/不带标签的模型实例。

我的问题:我无法(通过rest api)创建带有标签的模型的新实例。我可以毫无问题地获取模型实例。

我的models.py:

from taggit.managers import TaggableManager

class Task(models.Model):
    name = models.CharField(max_length=100, blank=False)
    ...
    tags = TaggableManager(blank=True)

    def get_tags(self):
        """ names() is a django-taggit method, returning a ValuesListQuerySet 
        (basically just an iterable) containing the name of each tag as a string
        """
        return self.tags.names()

    def __str__(self):
        return self.title

我的 serializers.py:

class TagsField(serializers.Field):
    """ custom field to serialize/deserialize TaggableManager instances.
    """
    def to_representation(self, value):
        """ in drf this method is called to convert a custom datatype into a primitive,
        serializable datatype.

        In this context, value is a plain django queryset containing a list of strings.
        This queryset is obtained thanks to get_tags() method on the Task model.

        Drf is able to serialize a queryset, hence we simply return it without doing nothing.
        """
        return value

    def to_internal_value(self, data):
        """ this method is called to restore a primitive datatype into its internal
        python representation.

        This method should raise a serializers.ValidationError if the data is invalid.
        """
        return data

class TaskSerializer(serializers.ModelSerializer):

    # tags field in Task model is implemented via TaggableManager class from django-taggit.
    # By default, drf is not able to serialize TaggableManager to json.
    # get_tags() is a method of the Task model class, which returns a Queryset containing
    # the list of tags as strings. This Queryset can be serialized without issues.
    tags = TagsField(source="get_tags")

    class Meta:
        model = Task
        fields = [
                "name",
                ...,
                "tags",
            ]

每当我尝试通过 POST api 创建任务模型的新实例时,都会收到以下错误:

TypeError at /taskdrop/v1/task/
Got a `TypeError` when calling `Task.objects.create()`. This may be because you have a writable field on the serializer class that is not a valid argument to `Task.objects.create()`. You may need to make the field read-only, or override the TaskSerializer.create() method to handle this correctly.
Original exception was:
 Traceback (most recent call last):
  File "/home/daniele/prj/ea/TaskDrop/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 948, in create
    instance = ModelClass._default_manager.create(**validated_data)
  File "/home/daniele/prj/ea/TaskDrop/venv/lib/python3.8/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/daniele/prj/ea/TaskDrop/venv/lib/python3.8/site-packages/django/db/models/query.py", line 420, in create
    obj = self.model(**kwargs)
  File "/home/daniele/prj/ea/TaskDrop/venv/lib/python3.8/site-packages/django/db/models/base.py", line 501, in __init__
    raise TypeError("%s() got an unexpected keyword argument '%s'" % (cls.__name__, kwarg))
TypeError: Task() got an unexpected keyword argument 'get_tags'

我现在有点卡住了...该字段绝对不是只读的,并且关于覆盖 TaskSerializer.create() 方法,我不知道该怎么做。

另外,我对 TagsField(serializers.Field) 与 .create() 方法覆盖有点困惑。据我了解,如果我创建了一个自定义序列化器字段,则不需要额外覆盖 .create()。

最后,我尝试使用django-taggit-serializer,但没有成功:模型已创建,但传递的标签丢失了。

我该如何解决这个问题?谢谢。

【问题讨论】:

    标签: python django django-rest-framework tags


    【解决方案1】:

    好的,我成功了。

    将解决方案留给其他人:

    我得到TypeError: Task() got an unexpected keyword argument 'get_tags' 的原因是因为 drf 试图使用 to_internal_value() 的返回值来填充我模型的“get_tags”字段。

    现在,“get_tags”只是我的模型任务类的方法名称,而不是真正的字段,因此出现错误。当我在序列化程序中使用 tags = TagsField(source="get_tags") 时,Drf 了解到 'get_tags' 作为字段名称。

    我以这种方式覆盖了我的序列化程序的 create() 方法解决了这个问题:

    class TaskSerializer(serializers.ModelSerializer):
    
        # tags field in Task model is implemented via TaggableManager class from django-taggit.
        # By default, drf is not able to serialize TaggableManager to json.
        # get_tags() is a method of the Task model class, which returns a Queryset containing
        # the list of tags as strings. This Queryset can be serialized without issues.
        tags = TagsField(source="get_tags")
        # variables = VariableSerializer()
    
        def create(self, validated_data):
            # using "source=get_tags" drf "thinks" get_tags is a real field name, so the
            # return value of to_internal_value() is used a the value of a key called "get_tags" inside validated_data dict. We need to remove it and handle the tags manually.
            tags = validated_data.pop("get_tags")
            task = Task.objects.create(**validated_data)
            task.tags.add(*tags)
    
            return task
    
        class Meta:
            model = Task
            # we exclude all those fields we simply receive from Socialminer
            # whenever we get a task or its status
            fields = [
                    "name",
                     ...
                    "tags",
                ]
    

    【讨论】:

      【解决方案2】:

      我认为您可能需要某种标签序列化程序设置。

      所以在你的TaskSerializer 中我会有:tags = TagSerializer(many=True, read_only=False)

      
      from serializers import (
          TagListSerializerField,
          TagSerializer
      )
      
      class TaskSerializer(TagSerializer, serializers.ModelSerializer):
      
          # tags field in Task model is implemented via TaggableManager class from django-taggit.
          # By default, drf is not able to serialize TaggableManager to json.
          # get_tags() is a method of the Task model class, which returns a Queryset containing
          # the list of tags as strings. This Queryset can be serialized without issues.
          tags = TagListSerializerField()
      
          class Meta:
              model = Task
              fields = [
                      "name",
                      ...,
                      "tags",
                  ]
      

      我在很多年前就实现了这个,你想要的TagListTagListSerializerFieldTagSerializer是这样的:

      import six
      import json
      
      from django.utils.translation import ugettext_lazy as _
      
      from rest_framework import serializer
      
      class TagList(list):
          def __init__(self, *args, **kwargs):
              pretty_print = kwargs.pop("pretty_print", True)
              list.__init__(self, *args, **kwargs)
              self.pretty_print = pretty_print
      
          def __add__(self, rhs):
              return TagList(list.__add__(self, rhs))
      
          def __getitem__(self, item):
              result = list.__getitem__(self, 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': _(
                  'Expected a list of items but got type "{input_type}".'),
              'invalid_json': _('Invalid json list. A tag list submitted in string'
                                ' form must be valid json.'),
              'not_a_str': _('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(TagListSerializerField, self).__init__(**kwargs)
      
              self.pretty_print = pretty_print
      
          def to_internal_value(self, value):
              if isinstance(value, six.string_types):
                  value = value.split(',')
      
              if not isinstance(value, list):
                  self.fail('not_a_list', input_type=type(value).__name__)
      
              for s in value:
                  if not isinstance(s, six.string_types):
                      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 TagSerializer(serializers.Serializer):
          def create(self, validated_data):
              to_be_tagged, validated_data = self._pop_tags(validated_data)
      
              tag_object = super(TaggitSerializer, self).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(TaggitSerializer, self).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)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-07-07
        • 1970-01-01
        • 2014-08-08
        • 2017-06-07
        • 2013-11-15
        • 1970-01-01
        • 2015-12-19
        相关资源
        最近更新 更多