带有ChoiceField的Django Rest框架


77

我的用户模型中有一些字段是选择字段,并试图找出如何最好地将其实现到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方法使用choice字段的显示值而不是代码,看起来像下面的JSON。

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

我将如何去做?上面的代码不起作用。在使用类似这种功能的GET之前,但是对于POST / PUT,它给了我错误。我正在寻找有关如何执行此操作的一般建议,这似乎很常见,但我找不到示例。或者是我做错了什么。



Answers:


11

此线程的更新,在最新版本的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__方法或添加任何其他程序包。


return self.GENDER_CHOICES[obj]在OP情况下应该这样吗?并且这比Django更好Model.get_FOO_display吗?
哈里·莫雷诺

您可以使用DRF的SerializerMethodField django-rest-framework.org/api-guide/fields/...如果你喜欢做,在串行水平。我会避免在模型级别将django内置验证与DRF的验证混合使用。
loicgasser

当@ kishan-mehta答案对我不起作用时。这个做了
罗伯特·约翰斯通

139

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')

3
如果想要所有可用选项的显示字符串怎么办?
哈肯·利德

3
事实证明,如果在ModelViewSet上使用http options方法,则rest-framework会显示选择,因此我不需要自定义序列化程序。
哈肯·里德(HåkenLid),2015年

1
@kishan使用get_render_display您将获得Male,如果您访问属性本身obj.gender,则将获得M
levi

6
当我使用drf v3.6.3时,gender = serializers.CharField(source='get_gender_display')效果很好。
大浪

1
您可以fuel = serializers.CharField(source='get_fuel_display', read_only=True)只显示人类可读的GET请求名称。POST请求仍将与代码一起使用(位于上ModelSerializer)。
Pierre Monico

25

我建议对自定义DRF序列化器字段使用django-models-utils

代码变为:

# 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

3
很久以前,这个问题已通过更简单的内置解决方案得到了回答。
2013年

10
2个区别:1)ChoicesField可以重复使用,并且2)它支持字段“ gender”的版本不再是只读的
nicolaspanel

鉴于选择的值本身太短,我将仅使用GENDER = Choices('Male', 'Female')and default=GENDER.Male,因为这无需创建自定义序列化器字段。
ergusto

5
我一直在寻找一种通过api读取和写入选择字段的方法,而这个答案很明确。接受的答案没有显示如何更新选择字段,而这个却有。
JLugao

仅实际支持正确写作的答案!
yspreen

13

也许您在您的某处需要这样的东西,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())))

2
我更喜欢这个答案,因为这样可以允许用户输入更多的选择键或值。只需更改if self._choices[i] == data:为即可if i == data or self._choices[i] == data:。从ChoiceField继承时,不需要覆盖,to_internal_value() 而只接受Choice键。
CK

7

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

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'
}

4

DRF3.1开始,有一个新的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 is 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"
},
...

0

我更喜欢@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重写是“丑陋的”,但我的目标始终是尽可能少地更改核心(以最大程度地实现前向兼容性)。

PS,如果您愿意allow_blank,DRF中有一个错误。最简单的解决方法是将以下内容添加到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)

PPS如果你有一堆的选择领域的所有需要映射的功能由@lechup注意到这一点,这样乘虚而入,以下内容添加到您的ModelSerializer不是它的Meta):

serializer_choice_field = MappedChoiceField

-1

我发现soup boy的方法是最好的。虽然我建议继承serializers.ChoiceField而不是继承serializers.Field。这样,您只需要重写to_representationmethod,其余的工作就像常规的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]

您有太多下划线呼唤super__init__
joebeeson
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.