【问题标题】:Django Rest Framework with ChoiceField带有 ChoiceField 的 Django Rest 框架
【发布时间】:2015-05-10 19:19:36
【问题描述】:

我的用户模型中有几个字段是选择字段,我正在尝试找出如何最好地将其实现到 Django Rest Framework 中。

下面是一些简化的代码来展示我在做什么。

# models.py
class User(AbstractUser):
    GENDER_CHOICES = (
        ('M', 'Male'),
        ('F', 'Female'),
    )

    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)


# serializers.py 
class UserSerializer(serializers.ModelSerializer):
    gender = serializers.CharField(source='get_gender_display')

    class Meta:
        model = User


# viewsets.py
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

基本上我想要做的是让 get/post/put 方法使用选择字段的显示值而不是代码,看起来像下面的 JSON。

{
  'username': 'newtestuser',
  'email': 'newuser@email.com',
  'first_name': 'first',
  'last_name': 'last',
  'gender': 'Male'
  // instead of 'gender': 'M'
}

我该怎么做呢?上面的代码不起作用。在我有这样的东西为 GET 工作之前,但对于 POST/PUT,它给了我错误。我正在寻找有关如何执行此操作的一般建议,这似乎很常见,但我找不到示例。要么这样,要么我做错了什么。

【问题讨论】:

标签: django django-rest-framework


【解决方案1】:

Django 提供Model.get_FOO_display 方法来获取字段的“人类可读”值:

class UserSerializer(serializers.ModelSerializer):
    gender = serializers.SerializerMethodField()

    class Meta:
        model = User

    def get_gender(self,obj):
        return obj.get_gender_display()

对于最新的 DRF (3.6.3) - 最简单的方法是:

gender = serializers.CharField(source='get_gender_display')

【讨论】:

  • 如果你想要所有可用选项的显示字符串怎么办?
  • 事实证明,如果您在 ModelViewSet 上使用 http 选项方法,rest-framework 会公开选项,因此我不需要自定义序列化程序。
  • @kishan 使用get_render_display 你会得到Male,如果你访问属性本身obj.gender,你会得到M
  • 当我使用 drf v3.6.3 时,gender = serializers.CharField(source='get_gender_display') 效果很好。
  • 您可以只使用fuel = serializers.CharField(source='get_fuel_display', read_only=True) 来仅显示GET 请求的人类可读名称。 POST 请求仍然可以使用代码(在 ModelSerializer 上)。
【解决方案2】:

此线程的更新,在最新版本的 DRF 中实际上有一个 ChoiceField

因此,如果您想返回 display_name,您需要做的就是继承 ChoiceField to_representation 方法,如下所示:

from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()

class ChoiceField(serializers.ChoiceField):

    def to_representation(self, obj):
        if obj == '' and self.allow_blank:
            return obj
        return self._choices[obj]

    def to_internal_value(self, data):
        # To support inserts with the value
        if data == '' and self.allow_blank:
            return ''

        for key, val in self._choices.items():
            if val == data:
                return key
        self.fail('invalid_choice', input=data)


class UserSerializer(serializers.ModelSerializer):
    gender = ChoiceField(choices=User.GENDER_CHOICES)

    class Meta:
        model = User

因此无需更改__init__ 方法或添加任何额外的包。

【讨论】:

  • 在 OP 的情况下应该是 return self.GENDER_CHOICES[obj] 吗?这比 Django 的 Model.get_FOO_display 更可取吗?
  • 您可以使用 DRF 的 SerializerMethodField django-rest-framework.org/api-guide/fields/… 如果您更喜欢在序列化程序级别这样做。我会避免将模型级别的 django 内置验证与 DRF 的验证混合。
  • 当@kishan-mehta 的回答对我不起作用时。这个做了
【解决方案3】:

我建议将django-models-utils 与自定义DRF serializer field 一起使用

代码变成:

# models.py
from model_utils import Choices

class User(AbstractUser):
    GENDER = Choices(
       ('M', 'Male'),
       ('F', 'Female'),
    )

    gender = models.CharField(max_length=1, choices=GENDER, default=GENDER.M)


# serializers.py 
from rest_framework import serializers

class ChoicesField(serializers.Field):
    def __init__(self, choices, **kwargs):
        self._choices = choices
        super(ChoicesField, self).__init__(**kwargs)

    def to_representation(self, obj):
        return self._choices[obj]

    def to_internal_value(self, data):
        return getattr(self._choices, data)

class UserSerializer(serializers.ModelSerializer):
    gender = ChoicesField(choices=User.GENDER)

    class Meta:
        model = User

# viewsets.py
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

【讨论】:

  • 这个问题很久以前就有一个更简单的内置解决方案回答了。
  • 2 个不同之处:1)ChoicesField 可以重复使用,2)它支持不再只读字段“gender”的版本
  • 鉴于选项的值本身很短,我将只使用 GENDER = Choices('Male', 'Female')default=GENDER.Male,因为这样可以绕过创建自定义序列化器字段的需要。
  • 我正在寻找一种通过 api 读取和写入选择字段的方法,这个答案成功了。接受的答案没有显示如何更新选择字段,这个可以。
  • 唯一真正支持正确书写的答案!
【解决方案4】:

您可能在 util.py 的某个地方需要这样的东西,并在涉及的任何序列化程序 ChoiceFields 中导入。

class ChoicesField(serializers.Field):
    """Custom ChoiceField serializer field."""

    def __init__(self, choices, **kwargs):
        """init."""
        self._choices = OrderedDict(choices)
        super(ChoicesField, self).__init__(**kwargs)

    def to_representation(self, obj):
        """Used while retrieving value for the field."""
        return self._choices[obj]

    def to_internal_value(self, data):
        """Used while storing value for the field."""
        for i in self._choices:
            if self._choices[i] == data:
                return i
        raise serializers.ValidationError("Acceptable values are {0}.".format(list(self._choices.values())))

【讨论】:

  • 我更喜欢这个答案,因为这可以让用户输入选择键或值。只需将if self._choices[i] == data: 更改为if i == data or self._choices[i] == data:。虽然从 ChoiceField 继承不需要覆盖 to_internal_value(),但只会接受选择键。
【解决方案5】:

以下解决方案适用于任何具有选项的字段,无需在序列化程序中为每个字段指定自定义方法:

from rest_framework import serializers

class ChoicesSerializerField(serializers.SerializerMethodField):
    """
    A read-only field that return the representation of a model field with choices.
    """

    def to_representation(self, value):
        # sample: 'get_XXXX_display'
        method_name = 'get_{field_name}_display'.format(field_name=self.field_name)
        # retrieve instance method
        method = getattr(value, method_name)
        # finally use instance method to return result of get_XXXX_display()
        return method()

例子:

给定:

class Person(models.Model):
    ...
    GENDER_CHOICES = (
        ('M', 'Male'),
        ('F', 'Female'),
    )
    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)

使用:

class PersonSerializer(serializers.ModelSerializer):
    ...
    gender = ChoicesSerializerField()

接收:

{
    ...
    'gender': 'Male'
}

代替:

{
    ...
    'gender': 'M'
}

【讨论】:

    【解决方案6】:

    DRF 3.1 以来,有一个名为customizing field mapping 的新API。我用它来将默认的 ChoiceField 映射更改为 ChoiceDisplayField:

    import six
    from rest_framework.fields import ChoiceField
    
    
    class ChoiceDisplayField(ChoiceField):
        def __init__(self, *args, **kwargs):
            super(ChoiceDisplayField, self).__init__(*args, **kwargs)
            self.choice_strings_to_display = {
                six.text_type(key): value for key, value in self.choices.items()
            }
    
        def to_representation(self, value):
            if value in ('', None):
                return value
            return {
                'value': self.choice_strings_to_values.get(six.text_type(value), value),
                'display': self.choice_strings_to_display.get(six.text_type(value), value),
            }
    
    class DefaultModelSerializer(serializers.ModelSerializer):
        serializer_choice_field = ChoiceDisplayField
    

    如果你使用DefaultModelSerializer

    class UserSerializer(DefaultModelSerializer):    
        class Meta:
            model = User
            fields = ('id', 'gender')
    

    你会得到类似的东西:

    ...
    
    "id": 1,
    "gender": {
        "display": "Male",
        "value": "M"
    },
    ...
    

    【讨论】:

      【解决方案7】:

      我迟到了,但我遇到了类似的情况并找到了不同的解决方案。

      当我尝试以前的解决方案时,我开始怀疑 GET 请求返回字段的显示名称是否有意义,但希望用户在 PUT 请求中向我发送字段的值(因为我的应用程序被翻译成许多语言,允许用户输入显示值将是灾难的秘诀)。

      我总是希望 API 中选择的输出与输入匹配 - 无论业务需求如何(因为这些需求很容易发生变化)

      所以我想出的解决方案(在 DRF 3.11 btw 上)是创建第二个只读字段,仅用于显示值。

      class UserSerializer(serializers.ModelSerializer):
          gender_display_value = serializers.CharField(
              source='get_gender_display', read_only=True
          )
      
          class Meta:
              model = User
              fields = (
                  "username",
                  "email",
                  "first_name",
                  "last_name",
                  "gender",
                  "gender_display_value",
              )
      

      这样我就可以保持一致的 API 签名,并且不必覆盖 DRF 的字段,也不必冒险将 Django 的内置模型验证与 DRF 的验证混为一谈。

      输出将是:

      {
        'username': 'newtestuser',
        'email': 'newuser@email.com',
        'first_name': 'first',
        'last_name': 'last',
        'gender': 'M',
        'gender_display_value': 'Male'
      }
      

      【讨论】:

        【解决方案8】:

        我发现soup boy 的方法是最好的。虽然我建议从serializers.ChoiceField 而不是serializers.Field 继承。这样你只需要重写to_representation 方法,其余的就像普通的ChoiceField一样工作。

        class DisplayChoiceField(serializers.ChoiceField):
        
            def __init__(self, *args, **kwargs):
                choices = kwargs.get('choices')
                self._choices = OrderedDict(choices)
                super(DisplayChoiceField, self).__init__(*args, **kwargs)
        
            def to_representation(self, obj):
                """Used while retrieving value for the field."""
                return self._choices[obj]
        

        【讨论】:

        • 您的__init__ 中调用super 的下划线过多
        【解决方案9】:

        我更喜欢@nicolaspanel 的答案,以保持字段可写。如果您使用此定义而不是他的ChoiceField,则您可以利用内置ChoiceField 中的任何/所有基础架构,同时映射来自str => int 的选择:

        class MappedChoiceField(serializers.ChoiceField):
        
            @serializers.ChoiceField.choices.setter
            def choices(self, choices):
                self.grouped_choices = fields.to_choices_dict(choices)
                self._choices = fields.flatten_choices_dict(self.grouped_choices)
                # in py2 use `iteritems` or `six.iteritems`
                self.choice_strings_to_values = {v: k for k, v in self._choices.items()}
        

        @property 覆盖“丑陋”,但我的目标始终是尽可能少地更改核心(以最大限度地提高前向兼容性)。

        附:如果你想allow_blank,在 DRF 中有一个bug。最简单的解决方法是将以下内容添加到MappedChoiceField

        def validate_empty_values(self, data):
            if data == '':
                if self.allow_blank:
                    return (True, None)
            # for py2 make the super() explicit
            return super().validate_empty_values(data)
        

        附言如果您有一堆需要映射的选择字段,请利用@lechup 指出的功能并将以下内容添加到您的ModelSerializer不是它的Meta):

        serializer_choice_field = MappedChoiceField
        

        【讨论】:

          猜你喜欢
          • 2019-05-01
          • 2023-03-17
          • 2018-08-06
          • 1970-01-01
          • 2016-06-25
          • 1970-01-01
          • 1970-01-01
          • 2016-04-19
          相关资源
          最近更新 更多