wake-up-neo.com

Django Rest Framework mit ChoiceField

Ich habe ein paar Felder in meinem Benutzermodell, die Auswahlfelder sind, und versuche, herauszufinden, wie dies am besten in Django Rest Framework implementiert werden kann.

Hier ist ein vereinfachter Code, um zu zeigen, was ich mache.

# 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

Im Wesentlichen versuche ich zu tun, dass die get/post/put-Methoden den Anzeigewert des Auswahlfelds anstelle des Codes verwenden und in etwa wie die unten stehende JSON aussehen.

{
  'username': 'newtestuser',
  'email': '[email protected]',
  'first_name': 'first',
  'last_name': 'last',
  'gender': 'Male'
  // instead of 'gender': 'M'
}

Wie würde ich das machen? Der obige Code funktioniert nicht. Vorher hatte ich so etwas für GET, aber für POST/PUT gab es Fehler. Ich bin auf der Suche nach allgemeinen Ratschlägen, es scheint allgemein üblich zu sein, aber ich kann keine Beispiele finden. Entweder das oder ich mache etwas furchtbar falsch.

41
awwester

Django bietet die Methode Model.get_FOO_display an, um den "lesbaren" Wert eines Felds zu erhalten:

class UserSerializer(serializers.ModelSerializer):
    gender = serializers.SerializerMethodField()

    class Meta:
        model = User

    def get_gender(self,obj):
        return obj.get_gender_display()

für die neueste DRF (3.6.3) - einfachste Methode ist:

gender = serializers.CharField(source='get_gender_display')
71
levi

Ich schlage vor, Django-models-utils mit einem benutzerdefinierten DRF-Serialisierungsfeld zu verwenden

Code wird zu:

# 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

In der Regel benötigen Sie so etwas irgendwo in Ihrem util.py und importieren Sie, in welche Serialisierer ChoiceFields einbezogen wird.

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

Die folgende Lösung funktioniert mit jedem Feld mit Auswahlmöglichkeiten, ohne dass im Serializer eine benutzerdefinierte Methode für jedes Feld angegeben werden muss:

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

Beispiel:

gegeben:

class Person(models.Model):
    ...
    GENDER_CHOICES = (
        ('M', 'Male'),
        ('F', 'Female'),
    )
    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)

benutzen:

class PersonSerializer(serializers.ModelSerializer):
    ...
    gender = ChoicesSerializerField()

bekommen:

{
    ...
    'gender': 'Male'
}

anstatt:

{
    ...
    'gender': 'M'
}
6
Mario Orlandi

Seit DRF 3.1 gibt es eine neue API namens Customizing Field Mapping . Ich habe es verwendet, um die Standard-ChoiceField-Zuordnung in ChoiceDisplayField zu ändern:

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

Wenn Sie DefaultModelSerializer verwenden:

class UserSerializer(DefaultModelSerializer):    
    class Meta:
        model = User
        fields = ('id', 'gender')

Sie erhalten so etwas wie:

...

"id": 1,
"gender": {
    "display": "Male",
    "value": "M"
},
...
4
lechup

Ein Update für diesen Thread, in den neuesten Versionen von DRF gibt es tatsächlich ein ChoiceField .

Alles, was Sie tun müssen, wenn Sie die display_name zurückgeben möchten, ist die Unterklasse ChoiceFieldto_representation wie folgt:

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

Es ist also nicht notwendig, die __init__-Methode zu ändern oder ein zusätzliches Paket hinzuzufügen.

3
loicgasser

Ich bevorzuge die Antwort von @nicolaspanel, um das Feld schreibbar zu halten. Wenn Sie diese Definition anstelle von ChoiceField verwenden, nutzen Sie die gesamte Infrastruktur in der integrierten ChoiceField, während Sie die Auswahl aus str => int zuordnen:

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

Die @ -Eigenschaftsüberschreibung ist "hässlich", aber mein Ziel ist es immer, so wenig wie möglich am Kern zu ändern (um die Vorwärtskompatibilität zu maximieren). 

P.S. Wenn Sie allow_blank möchten, gibt es in DRF ein bug . Die einfachste Problemumgehung besteht darin, Folgendes zu MappedChoiceField hinzuzufügen:

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. Wenn Sie eine Reihe von Auswahlfeldern haben, die alle zugeordnet werden müssen, nutzen Sie die von @lechup notierte Funktion und fügen Sie Ihrer ModelSerializer (nicht ihre Meta) Folgendes hinzu:

serializer_choice_field = MappedChoiceField
0
claytond

Ich fand den Ansatz von soup boy am besten. Ich würde jedoch vorschlagen, von serializers.ChoiceField und nicht von serializers.Field zu erben. Auf diese Weise müssen Sie nur die to_representation-Methode überschreiben, und der Rest funktioniert wie ein reguläres 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]
0
rajat404