Подтвердить что ты не робот

Django Rest Framework с ChoiceField

У меня есть несколько полей в моей модели пользователей, которые являются полями выбора, и я пытаюсь выяснить, как наилучшим образом реализовать это в Django Rest Framework.

Ниже приведен некоторый упрощенный код, чтобы показать, что я делаю.

# 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

По сути, я пытаюсь сделать, чтобы методы get/post/put использовали отображаемое значение поля выбора вместо кода, выглядя примерно как JSON ниже.

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

Как бы я это сделал? Вышеприведенный код не работает. Раньше у меня было что-то подобное для GET, но для POST/PUT это давало мне ошибки. Я ищу общие советы о том, как это сделать, похоже, что это будет что-то общее, но я не могу найти примеры. Либо это, либо я делаю что-то ужасно неправильно.

4b9b3361

Ответ 1

Django предоставляет метод Model.get_FOO_display для получения "читабельного" значения поля:

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

    class Meta:
        model = User

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

для последней DRF (3.6.3) - самый простой метод:

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

Ответ 2

Я предлагаю использовать django-models-utils с пользовательским полем сериализатора DRF

Код становится:

# 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

Ответ 3

Probalbly вам нужно что-то подобное в вашем util.py и импортировать в зависимости от того, какие сериализаторы 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())))

Ответ 4

Следующее решение работает с любым полем с выбором, без необходимости указывать в сериализаторе настраиваемый метод для каждого:

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

Пример:

Дано:

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

использование:

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

для получения:

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

вместо:

{
    ...
    'gender': 'M'
}

Ответ 5

Так как DRF 3.1 существует новый API, называемый настройка сопоставления полей. Я использовал его для изменения отображения ChoiceField по умолчанию на 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

Если вы используете DefaultModelSerializer:

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

Вы получите что-то вроде:

...

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

Ответ 6

Я нашел подход soup boy лучшим. Хотя я предлагаю унаследовать от serializers.ChoiceField, а не serializers.Field. Таким образом вам нужно только переопределить метод to_representation, а остальное работает как обычный 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]

Ответ 7

Я предпочитаю ответ @nicolaspanel, чтобы поле оставалось доступным для записи. Если вы используете это определение вместо его ChoiceField, вы используете любую/всю инфраструктуру во встроенном ChoiceField, отображая выбор из 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()}

Переопределение @property "безобразно", но моя цель всегда состоит в том, чтобы как можно меньше менять ядро (чтобы максимизировать прямую совместимость).

PS если вы хотите allow_blank, есть ошибка в DRF. Самый простой обходной путь - добавить следующее в 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 Если у вас есть куча полей выбора, которые необходимо сопоставить всем, воспользуйтесь функцией, отмеченной @lechup, и добавьте следующее в ModelSerializer (не его Meta):

serializer_choice_field = MappedChoiceField

Ответ 8

Обновление для этой темы, в последних версиях DRF есть фактически ChoiceField.

Поэтому все, что вам нужно сделать, если вы хотите вернуть display_name это ChoiceField подкласс метода ChoiceField to_representation следующим образом:

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

Поэтому нет необходимости изменять метод __init__ или добавлять какой-либо дополнительный пакет.