Cadre de repos Django avec ChoiceField
Dans mon modèle utilisateur, quelques champs sont des champs de choix et j'essaie de trouver la meilleure façon de les implémenter dans Django Rest Framework.
Ci-dessous, un code simplifié pour montrer ce que je fais.
# 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
Ce que j'essaie essentiellement de faire, c'est que les méthodes get/post/put utilisent la valeur d'affichage du champ de choix au lieu du code, ce qui ressemble au JSON ci-dessous.
{
'username': 'newtestuser',
'email': 'newuser@email.com',
'first_name': 'first',
'last_name': 'last',
'gender': 'Male'
// instead of 'gender': 'M'
}
Comment pourrais-je m'y prendre? Le code ci-dessus ne fonctionne pas. Auparavant, quelque chose comme cela fonctionnait pour GET, mais pour POST/PUT, cela me donnait des erreurs. Je cherche un conseil général sur la façon de procéder. Il semble que ce soit quelque chose de commun, mais je ne trouve pas d’exemples. Soit ça ou je fais quelque chose de terriblement faux.
Django fournit la méthode Model.get_FOO_display
pour obtenir la valeur "lisible par un humain" d'un champ:
class UserSerializer(serializers.ModelSerializer):
gender = serializers.SerializerMethodField()
class Meta:
model = User
def get_gender(self,obj):
return obj.get_gender_display()
pour le dernier DRF (3.6.3) - la méthode la plus simple est:
gender = serializers.CharField(source='get_gender_display')
Je suggère d'utiliser Django-models-utils avec un champ personnalisé Champ de sérialiseur DRF
Le code devient:
# 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
Probablement vous avez besoin de quelque chose comme ceci quelque part dans votre util.py
et vous importez quel que soit le sérialiseur ChoiceFields
impliqué.
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())))
La solution suivante fonctionne avec tous les champs avec choix, sans qu'il soit nécessaire de spécifier dans le sérialiseur une méthode personnalisée pour chacun:
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()
Exemple:
donné:
class Person(models.Model):
...
GENDER_CHOICES = (
('M', 'Male'),
('F', 'Female'),
)
gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
utilisation:
class PersonSerializer(serializers.ModelSerializer):
...
gender = ChoicesSerializerField()
recevoir:
{
...
'gender': 'Male'
}
au lieu de:
{
...
'gender': 'M'
}
Depuis DRF
3.1, une nouvelle API appelée personnalisation du mappage de champs . Je l'ai utilisé pour changer le mappage ChoiceField par défaut en 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
Si vous utilisez DefaultModelSerializer
:
class UserSerializer(DefaultModelSerializer):
class Meta:
model = User
fields = ('id', 'gender')
Vous obtiendrez quelque chose comme:
...
"id": 1,
"gender": {
"display": "Male",
"value": "M"
},
...
Une mise à jour pour ce fil de discussion. Dans les dernières versions de DRF, il existe en fait un ChoiceField .
Donc, tout ce que vous devez faire si vous voulez renvoyer le display_name
est de sous-classer la méthode ChoiceField
to_representation
comme ceci:
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
Il n’est donc pas nécessaire de changer la méthode __init__
ni d’ajouter de paquet supplémentaire.
Je préfère la réponse de @nicolaspanel pour que le champ reste accessible en écriture. Si vous utilisez cette définition à la place de sa ChoiceField
, vous tirez parti de toute l'infrastructure de la ChoiceField
intégrée tout en mappant les choix 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 substitution de @property est "moche" mais mon objectif est toujours de modifier le moins possible le noyau (pour maximiser la compatibilité en aval).
P.S. si vous voulez allow_blank
, il y a un bug dans DRF. La solution la plus simple consiste à ajouter ce qui suit à 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. Si vous avez un ensemble de champs de choix qui doivent tous être mappés, profitez de la fonctionnalité notée par @lechup et ajoutez ce qui suit à votre ModelSerializer
(pas sa Meta
):
serializer_choice_field = MappedChoiceField
J'ai trouvé l'approche de soup boy
comme étant la meilleure. Bien que je suggère d’hériter de serializers.ChoiceField
plutôt que de serializers.Field
. De cette façon, il vous suffit de remplacer la méthode to_representation
et le reste fonctionne comme 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]