带有 ChoiceField 的 Django Rest 框架
Django Rest Framework with ChoiceField
我的用户模型中有几个字段是选择字段,我正在尝试弄清楚如何最好地将其实现到 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 提供了Model.get_FOO_display
方法来获取字段的"human-readable"值:
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')
我建议使用django-models-utils with a custom 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
以下解决方案适用于任何有选择的字段,无需在序列化程序中为每个字段指定自定义方法:
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'
}
可能您在 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())))
自 DRF
3.1 以来有新的 API 称为 customizing field mapping。我用它将默认的 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
中基础结构的 any/all,同时映射来自 str
=> [= 的选择16=]:
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()}
@属性 覆盖是 "ugly" 但我的目标始终是尽可能少地更改核心(以最大限度地提高向前兼容性)。
P.S。如果你想 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)
P.P.S。如果您有一堆选择字段都需要映射到此,请利用@lechup 指出的功能并将以下内容添加到您的 ModelSerializer
(not 它的Meta
):
serializer_choice_field = MappedChoiceField
此线程的更新,在最新版本的 DRF 中实际上有一个 ChoiceField。
因此,如果您想要 return 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 字段的显示名称发出 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'
}
我的用户模型中有几个字段是选择字段,我正在尝试弄清楚如何最好地将其实现到 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 提供了Model.get_FOO_display
方法来获取字段的"human-readable"值:
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')
我建议使用django-models-utils with a custom 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
以下解决方案适用于任何有选择的字段,无需在序列化程序中为每个字段指定自定义方法:
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'
}
可能您在 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())))
自 DRF
3.1 以来有新的 API 称为 customizing field mapping。我用它将默认的 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
中基础结构的 any/all,同时映射来自 str
=> [= 的选择16=]:
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()}
@属性 覆盖是 "ugly" 但我的目标始终是尽可能少地更改核心(以最大限度地提高向前兼容性)。
P.S。如果你想 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)
P.P.S。如果您有一堆选择字段都需要映射到此,请利用@lechup 指出的功能并将以下内容添加到您的 ModelSerializer
(not 它的Meta
):
serializer_choice_field = MappedChoiceField
此线程的更新,在最新版本的 DRF 中实际上有一个 ChoiceField。
因此,如果您想要 return 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 字段的显示名称发出 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'
}