Django Rest Framework with ChoiceField

I have a few fields in my user model that are choice fields and am trying to figure out how to best implement that into Django Rest Framework.

Below is some simplified code to show what I'm doing.

# 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

Essentially what I'm trying to do is to have the get/post/put methods use the display value of the choice field instead of the code, looking something like the below JSON.

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

How would I go about doing that? The above code does not work. Before I had something like this working for GET, but for POST/PUT it was giving me errors. I'm looking for general advice on how to do this, it seems like it would be something common, but I can't find examples. Either that or I'm doing something terribly wrong.

102459 次浏览

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

我建议使用自定义 DRF 序列化程序字段Django-模型-工具

代码变成:

# 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

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

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

Probalbly you need something like this somewhere in your util.py and import in whichever serializers ChoiceFields are involved.

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

自从 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 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"
},
...

我发现 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]

我更喜欢@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 中有一个 臭虫。最简单的解决方法是在 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

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

我比赛迟到了,但是我面临着类似的情况,并且达成了一个不同的解决方案。

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

无论业务需求如何(因为这些需求很容易改变) ,我总是期望 API 中的选项的输出与输入 匹配

所以我想到的解决方案(顺便说一下,在 DRF 3.11上)是为显示值创建第二个只读字段。

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 的验证混淆在一起。

The output will be:

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