Django Rest Framework із ChoiceField


82

У моїй користувацькій моделі є кілька полів, які є полями вибору, і я намагаюся зрозуміти, як найкраще реалізувати це в 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': 'newuser@email.com',
  'first_name': 'first',
  'last_name': 'last',
  'gender': 'Male'
  // instead of 'gender': 'M'
}

Як би я це зробив? Наведений вище код не працює. Раніше у мене щось подібне працювало для GET, але для POST / PUT це давало мені помилки. Я шукаю загальну пораду, як це зробити, здається, це було б щось загальне, але я не можу знайти приклади. Або це, або я роблю щось жахливо неправильне.



stackoverflow.com/questions/64619906 / ... loicgasser ласка , допоможіть мені в цьому
Аднана Rizwee

Відповіді:


13

Оновлення для цього потоку, в останніх версіях DRF насправді є ChoiceField .

Отже, все, що вам потрібно зробити, якщо ви хочете повернути метод display_namesubclass, 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):
        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

Тому немає необхідності змінювати __init__метод або додавати будь-який додатковий пакет.


це має бути return self.GENDER_CHOICES[obj]у випадку з ОП? і чи це краще, ніж у Джанго Model.get_FOO_display?
Harry Moreno

Ви можете використовувати DRF's SerializerMethodField django-rest-framework.org/api-guide/fields/…, якщо вам більше подобається робити це на рівні серіалізатора. Я не хотів би змішувати вбудовану перевірку django на рівні моделі з перевіркою DRF.
loicgasser,

Коли відповідь @ kishan-mehta у мене не спрацювала. Цей зробив
Роберт Джонстоун

144

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

4
Що робити, якщо ви хочете відобразити рядки з усіх доступних варіантів?
Håken Lid

4
Виявляється, rest-framework надає можливість вибору, якщо ви використовуєте метод параметрів http на ModelViewSet, тому мені не потрібно було налаштовувати серіалізатор.
Håken Lid

1
@kishan за допомогою get_render_displayви отримаєте Male, якщо ви отримаєте доступ до самого атрибуту obj.gender, ви отримаєтеM
levi

6
оскільки я використовую drf v3.6.3, gender = serializers.CharField(source='get_gender_display')це добре працює.
даланг

1
Ви можете просто зробити, fuel = serializers.CharField(source='get_fuel_display', read_only=True)щоб відображати лише читабельне ім’я для GETзапитів. POSTзапити все одно працюватимуть з кодом (увімкнено ModelSerializer).
П'єр Моніко,

25

Я пропоную використовувати 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
на це питання давно відповіли набагато простішим вбудованим рішенням.
Аввестер

10
2 відмінності: 1) ChoicesField можна використовувати повторно і 2) він підтримує видання для поля "стать", яке більше не
доступне

Враховуючи, що значення для вибору самі по собі настільки короткі, я б просто їх використав GENDER = Choices('Male', 'Female')і default=GENDER.Male, оскільки це обходить необхідність у створенні власного поля серіалізатора.
ergusto

5
Я шукав спосіб прочитати та написати поле вибору через api, і ця відповідь прибила його. Прийнята відповідь не показує, як оновити поле вибору, це робить.
JLugao

Тільки відповідь, яка насправді підтримує належне письмо!
yspreen

13

Ймовірно, вам потрібно щось подібне десь у вашому пристрої 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())))

2
Я віддаю перевагу цій відповіді, оскільки це може дозволити користувачам вводити будь-який ключ вибору чи значення. Просто переодягнувшись if self._choices[i] == data:у if i == data or self._choices[i] == data:. У той час як успадкування від ChoiceField не потрібно замінювати, to_internal_value() а приймає лише ключ вибору.
CK

8

Наступне рішення працює з будь-яким полем з вибором, без необхідності вказувати в серіалізаторі спеціальний метод для кожного:

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'
}

4

З DRF3.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"
},
...

0

Я вважаю за краще відповідь @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

-1

Я виявив 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]

У вас занадто багато підкреслень, які super__init__
викликують
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.