example - Django Rest Framework con ChoiceField
django rest framework serializer (8)
Desde
DRF
3.1 hay una nueva API llamada
personalización de mapeo de campo
.
Lo usé para cambiar la asignación predeterminada de ChoiceField a ChoiceDisplayField:
class UserSerializer(DefaultModelSerializer):
class Meta:
model = User
fields = (''id'', ''gender'')
Si usa
DefaultModelSerializer
:
...
"id": 1,
"gender": {
"display": "Male",
"value": "M"
},
...
Obtendrás algo como:
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):
return self._choices[obj]
class UserSerializer(serializers.ModelSerializer):
gender = ChoiceField(choices=User.GENDER_CHOICES)
class Meta:
model = User
Tengo algunos campos en mi modelo de usuario que son campos de elección y estoy tratando de descubrir cómo implementarlo mejor en Django Rest Framework.
A continuación hay un código simplificado para mostrar lo que estoy haciendo.
# 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
Esencialmente, lo que intento hacer es hacer que los métodos get / post / put utilicen el valor de visualización del campo de elección en lugar del código, con un aspecto similar al JSON a continuación.
{
''username'': ''newtestuser'',
''email'': ''[email protected]'',
''first_name'': ''first'',
''last_name'': ''last'',
''gender'': ''Male''
// instead of ''gender'': ''M''
}
¿Cómo voy a hacer eso? El código anterior no funciona. Antes tenía algo como esto trabajando para GET, pero para POST / PUT me daba errores. Estoy buscando consejos generales sobre cómo hacer esto, parece que sería algo común, pero no puedo encontrar ejemplos. O eso o estoy haciendo algo terriblemente mal.
Django proporciona el método
Model.get_FOO_display
para obtener el valor "legible para humanos" de un campo:
class UserSerializer(serializers.ModelSerializer):
gender = serializers.SerializerMethodField()
class Meta:
model = User
def get_gender(self,obj):
return obj.get_gender_display()
para el último DRF (3.6.3) - el método más sencillo es:
gender = serializers.CharField(source=''get_gender_display'')
Encontré
soup boy
el enfoque del
soup boy
es el mejor.
Aunque sugeriría heredar de
serializers.ChoiceField
lugar de
serializers.Field
.
De esta manera, solo necesita anular el método
to_representation
y el resto funciona como un ChoiceField normal.
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]
La siguiente solución funciona con cualquier campo con opciones, sin necesidad de especificar en el serializador un método personalizado para cada uno:
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()
Ejemplo:
dado:
class Person(models.Model):
...
GENDER_CHOICES = (
(''M'', ''Male''),
(''F'', ''Female''),
)
gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
utilizar:
class PersonSerializer(serializers.ModelSerializer):
...
gender = ChoicesSerializerField()
para recibir:
{
...
''gender'': ''Male''
}
en lugar de:
{
...
''gender'': ''M''
}
Prefiero la respuesta de @nicolaspanel para mantener el campo de escritura.
Si usa esta definición en lugar de su
ChoiceField
, aprovechará toda / toda la infraestructura en el
ChoiceField
mientras
ChoiceField
las opciones de
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()}
La anulación @property es "fea", pero mi objetivo siempre es cambiar la menor cantidad posible del núcleo (para maximizar la compatibilidad hacia adelante).
PD: si quieres
allow_blank
, hay un
bug
en DRF.
La solución más simple es agregar lo siguiente a
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 Si tiene un montón de campos de elección que todos necesitan ser mapeados, aproveche la característica anotada por @lechup y agregue lo siguiente a su
ModelSerializer
(
no
su
Meta
):
serializer_choice_field = MappedChoiceField
Probablemente necesite algo como esto en algún lugar de su
util.py
e importar en cualquier serializador que
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())))
Sugiero usar django-models-utils con un campo de serializador DRF personalizado
El código se convierte en:
# 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
Una actualización para este hilo, en las últimas versiones de DRF, en realidad hay un ChoiceField .
Por lo tanto, todo lo que debe hacer si desea devolver el
display_name
es subclasificar
ChoiceField
to_representation
método de presentación como este:
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
Por lo tanto, no es necesario cambiar el método
__init__
ni agregar ningún paquete adicional.