web-dev-qa-db-fra.com

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.

41
awwester

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

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
14
nicolaspanel

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

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'
}
6
Mario Orlandi

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"
},
...
4
lechup

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 ChoiceFieldto_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.

3
loicgasser

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
0
claytond

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]
0
rajat404