Django Rest Framework mit ChoiceField

77

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

Unten finden Sie einen vereinfachten Code, der zeigt, was ich tue.

# 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, dass die Methoden get / post / put den Anzeigewert des Auswahlfelds anstelle des Codes verwenden und ungefähr so ​​aussehen wie der folgende JSON.

{
  '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. Bevor ich so etwas für GET hatte, aber für POST / PUT gab es mir Fehler. Ich suche nach allgemeinen Ratschlägen, wie das geht. Es scheint, als wäre es etwas Übliches, aber ich kann keine Beispiele finden. Entweder das oder ich mache etwas schrecklich Falsches.

awwester
quelle
stackoverflow.com/questions/64619906/… loicgasser bitte hilf mir dabei
Adnan Rizwee

Antworten:

11

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

Alles, was Sie tun müssen, um das zurückzugeben, display_nameist eine Unterklassenmethode ChoiceField to_representationwie 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):
        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

Es ist also nicht erforderlich, die __init__Methode zu ändern oder ein zusätzliches Paket hinzuzufügen.

Loicgasser
quelle
sollte das return self.GENDER_CHOICES[obj]in OPs Fall sein? und ist das Djangos vorzuziehen Model.get_FOO_display?
Harry Moreno
Sie können DRFs SerializerMethodField django-rest-framework.org/api-guide/fields/… verwenden, wenn Sie dies auf Serializer-Ebene bevorzugen. Ich würde es vermeiden, die in Django integrierte Validierung auf Modellebene mit der Validierung von DRF zu mischen.
Loicgasser
Als @ kishan-mehta Antwort hat bei mir nicht funktioniert. Dieser tat es
Robert Johnstone
139

Django bietet die Model.get_FOO_displayMethode, um den "lesbaren" Wert eines Feldes 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) ist die einfachste Methode:

gender = serializers.CharField(source='get_gender_display')
Levi
quelle
3
Was ist, wenn Sie die Anzeigezeichenfolgen aller verfügbaren Optionen möchten?
Håken Deckel
3
Es stellt sich heraus, dass das Rest-Framework die Auswahlmöglichkeiten offenlegt, wenn Sie die http-Optionsmethode in einem ModelViewSet verwenden, sodass ich den Serializer nicht anpassen musste.
Håken Deckel
1
@kishan mit get_render_displayIhnen erhalten Male, wenn Sie auf das Attribut selbst zugreifen obj.gender, erhalten SieM
Levi
6
da ich drf v3.6.3 benutze, gender = serializers.CharField(source='get_gender_display')funktioniert es gut.
Dalang
1
Sie können nur fuel = serializers.CharField(source='get_fuel_display', read_only=True)den für Menschen lesbaren Namen für GETAnforderungen anzeigen . POSTAnfragen funktionieren weiterhin mit dem Code (on ModelSerializer).
Pierre Monico
25

Ich schlage vor, django-models-utils mit einem benutzerdefinierten DRF-Serializer-Feld zu verwenden

Code wird:

# 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
nicolaspanel
quelle
3
Diese Frage wurde vor langer Zeit mit einer viel einfacheren integrierten Lösung beantwortet.
Awwester
10
2 Unterschiede: 1) ChoicesField kann wiederverwendet werden und 2) es unterstützt Editionen für das Feld "gender", die nicht mehr
schreibgeschützt
Angesichts der Tatsache, dass die Werte für die Auswahlmöglichkeiten selbst so kurz sind, würde ich nur verwenden GENDER = Choices('Male', 'Female')und default=GENDER.Male, da dies die Notwendigkeit umgeht, ein benutzerdefiniertes Serializer-Feld zu erstellen.
Ergusto
5
Ich suchte nach einer Möglichkeit, ein Auswahlfeld über die API zu lesen und zu schreiben, und diese Antwort traf es. Die akzeptierte Antwort zeigt nicht, wie ein Auswahlfeld aktualisiert wird.
JLugao
Nur eine Antwort, die das Schreiben tatsächlich unterstützt!
yspreen
13

Wahrscheinlich brauchen Sie so etwas irgendwo in Ihrem util.pyund importieren in welche Serializer auch ChoiceFieldsimmer involviert sind.

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())))
Kishan Mehta
quelle
2
Ich bevorzuge diese Antwort, da Benutzer dadurch einen Auswahlschlüssel oder -wert eingeben können. Durch einfaches Wechseln if self._choices[i] == data:in if i == data or self._choices[i] == data:. Während des Erbens von ChoiceField muss es nicht überschrieben werden, to_internal_value() sondern akzeptiert nur den Auswahlschlüssel .
CK
8

Die folgende Lösung funktioniert mit jedem Feld mit Auswahlmöglichkeiten, ohne dass im Serializer für jedes Feld eine benutzerdefinierte Methode 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)

verwenden:

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

bekommen:

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

anstatt:

{
    ...
    'gender': 'M'
}
Mario Orlandi
quelle
4

Seit DRF3.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 verwenden DefaultModelSerializer:

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

Sie erhalten so etwas wie:

...

"id": 1,
"gender": {
    "display": "Male",
    "value": "M"
},
...
Lechup
quelle
0

Ich bevorzuge die Antwort von @nicolaspanel, um das Feld beschreibbar zu halten. Wenn Sie diese Definition anstelle seiner verwenden ChoiceField, nutzen Sie die gesamte integrierte Infrastruktur, ChoiceFieldwährend Sie die Auswahlmöglichkeiten von 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()}

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

PS Wenn Sie möchten allow_blank, gibt es einen Fehler in DRF. Die einfachste Problemumgehung besteht darin, Folgendes hinzuzufügen 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 Wenn Sie eine Reihe von Auswahlfeldern haben, die alle auf diese Weise zugeordnet werden müssen, nutzen Sie die von @lechup angegebene Funktion und fügen Sie Folgendes zu Ihrem ModelSerializer( nicht seinem Meta) hinzu:

serializer_choice_field = MappedChoiceField
Claytond
quelle
-1

Ich fand soup boyden Ansatz am besten. Obwohl ich vorschlagen würde, serializers.ChoiceFieldeher von als zu erben serializers.Field. Auf diese Weise müssen Sie nur die to_representationMethode überschreiben und der Rest funktioniert wie ein normales 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]
rajat404
quelle
Sie haben einen zu vielen Unterstriche superin Ihrem__init__
Joebeeson