Django rest frame, використовуйте різні серіалізатори в одному ModelViewSet


196

Я хотів би надати два різні серіалізатори і все-таки мати змогу скористатися всіма можливостями ModelViewSet:

  • Переглядаючи список об'єктів, я хотів би, щоб кожен об'єкт мав URL-адресу, яка переспрямовує його деталі, а кожне інше відношення з'являється за __unicode __допомогою цільової моделі;

приклад:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • Переглядаючи деталі об'єкта, я хотів би використовувати типовий HyperlinkedModelSerializer

приклад:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "http://127.0.0.1:8000/database/utenti/3/",
  "accesso": "CHI",
  "membri": [
    "http://127.0.0.1:8000/database/utenti/3/",
    "http://127.0.0.1:8000/database/utenti/4/",
    "http://127.0.0.1:8000/database/utenti/5/",
    "http://127.0.0.1:8000/database/utenti/6/",
    "http://127.0.0.1:8000/database/utenti/7/"
  ]
}

Мені вдалося зробити все це, як я хочу, таким чином:

serializers.py

# serializer to use when showing a list
class ListaGruppi(serializers.HyperlinkedModelSerializer):
    membri = serializers.RelatedField(many = True)
    creatore = serializers.RelatedField(many = False)

    class Meta:
        model = models.Gruppi

# serializer to use when showing the details
class DettaglioGruppi(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Gruppi

views.py

class DualSerializerViewSet(viewsets.ModelViewSet):
    """
    ViewSet providing different serializers for list and detail views.

    Use list_serializer and detail_serializer to provide them
    """
    def list(self, *args, **kwargs):
        self.serializer_class = self.list_serializer
        return viewsets.ModelViewSet.list(self, *args, **kwargs)

    def retrieve(self, *args, **kwargs):
        self.serializer_class = self.detail_serializer
        return viewsets.ModelViewSet.retrieve(self, *args, **kwargs)

class GruppiViewSet(DualSerializerViewSet):
    model = models.Gruppi
    list_serializer = serializers.ListaGruppi
    detail_serializer = serializers.DettaglioGruppi

    # etc.

В основному я виявляю, коли користувач вимагає перегляду списку або детального перегляду та змінюється serializer_classвідповідно до моїх потреб. Я не дуже задоволений цим кодом, він виглядає як брудний злом і, що найголовніше, що робити, якщо двоє користувачів запитують список і деталі в один і той же момент?

Чи є кращий спосіб досягти цього, використовуючи ModelViewSetsчи мені потрібно відмовитися GenericAPIView?

EDIT:
Ось як це зробити за допомогою спеціальної бази ModelViewSet:

class MultiSerializerViewSet(viewsets.ModelViewSet):
    serializers = { 
        'default': None,
    }

    def get_serializer_class(self):
            return self.serializers.get(self.action,
                        self.serializers['default'])

class GruppiViewSet(MultiSerializerViewSet):
    model = models.Gruppi

    serializers = {
        'list':    serializers.ListaGruppi,
        'detail':  serializers.DettaglioGruppi,
        # etc.
    }

як ти це остаточно реалізував? Використовуючи спосіб, запропонований користувачем2734679, або використовуючи GenericAPIView?
andilabs

Як пропонує користувач2734679; Я створив загальний ViewSet, додаючи словник, щоб вказати серіалізатор для кожної дії та серіалізатор за замовчуванням, якщо не вказано
BlackBear,

У мене є аналогічна проблема ( stackoverflow.com/questions/24809737/… ), і зараз вона закінчилася ( gist.github.com/andilab/a23a6370bd118bf5e858 ), але я не дуже задоволений цим.
andilabs

1
Створив для цього невеликий пакет. github.com/Darwesh27/drf-custom-viewsets
Аділ Малік

1
Добрий метод вилучення.
gzerone

Відповіді:


288

Перевизначте свій get_serializer_classметод. Цей метод використовується у вашій моделі mixins для отримання відповідного класу Serializer.

Зауважте, що існує також get_serializerметод, який повертає екземпляр правильного серіалізатора

class DualSerializerViewSet(viewsets.ModelViewSet):
    def get_serializer_class(self):
        if self.action == 'list':
            return serializers.ListaGruppi
        if self.action == 'retrieve':
            return serializers.DettaglioGruppi
        return serializers.Default # I dont' know what you want for create/destroy/update.                

1
Це чудово, дякую! Я перевершив get_serializer_class, хоча
BlackBear

15
ПОПЕРЕДЖЕННЯ. Помилка django rest не встановлює параметр self.action, тому ця функція буде винятком виключення. Ви можете скористатися відповіддю gonz або скористатисяif hasattr(self, 'action') and self.action == 'list'
Том Лейс,

Створіть для цього невеликий пакет pypi. github.com/Darwesh27/drf-custom-viewsets
Аділ Малік

Як нам отримати pkзапитуваний об’єкт, якщо дія retrieve?
Pranjal Mittal

Моя самодіяльність - Ні. Може хтось скаже мені, чому?
Какаджі

86

Ви можете вважати цей міксин корисним, він переосмислює метод get_serializer_class і дозволяє оголосити дік, який відображає клас дії та серіалізатор або резервний характер до звичайної поведінки.

class MultiSerializerViewSetMixin(object):
    def get_serializer_class(self):
        """
        Look for serializer class in self.serializer_action_classes, which
        should be a dict mapping action name (key) to serializer class (value),
        i.e.:

        class MyViewSet(MultiSerializerViewSetMixin, ViewSet):
            serializer_class = MyDefaultSerializer
            serializer_action_classes = {
               'list': MyListSerializer,
               'my_action': MyActionSerializer,
            }

            @action
            def my_action:
                ...

        If there's no entry for that action then just fallback to the regular
        get_serializer_class lookup: self.serializer_class, DefaultSerializer.

        """
        try:
            return self.serializer_action_classes[self.action]
        except (KeyError, AttributeError):
            return super(MultiSerializerViewSetMixin, self).get_serializer_class()

Створив для цього невеликий пакет. github.com/Darwesh27/drf-custom-viewsets
Аділ Малик

15

Ця відповідь така сама, як прийнята відповідь, але я вважаю за краще це робити.

Загальні погляди

get_serializer_class(self):

Повертає клас, який слід використовувати для серіалізатора. За замовчуванням повертається serializer_classатрибут.

Може бути відмінено для надання динамічної поведінки, наприклад, використання різних серіалізаторів для операцій читання та запису або надання різних серіалізаторів для різних типів користувачів. атрибут serializer_class

class DualSerializerViewSet(viewsets.ModelViewSet):
    # mapping serializer into the action
    serializer_classes = {
        'list': serializers.ListaGruppi,
        'retrieve': serializers.DettaglioGruppi,
        # ... other actions
    }
    default_serializer_class = DefaultSerializer # Your default serializer

    def get_serializer_class(self):
        return self.serializer_classes.get(self.action, self.default_serializer_class)

Неможливо використовувати його, оскільки це говорить мені, що мій погляд не має атрибута "дія". Це схоже на ProductIndex (generics.ListCreateAPIView). Чи означає це, що вам абсолютно потрібно передавати набори перегляду як аргумент або є спосіб це зробити за допомогою переглядів API generics?
Себ

1
пізня відповідь на коментар @Seb - можливо, хтось може отримати з цього прибуток :) Приклад використовує ViewSets, а не Перегляди :)
fanny

Таким чином , в поєднанні з цим постом stackoverflow.com/questions/32589087 / ... , ViewSets , здається, шлях , щоб мати більше контролю над різними видами і генерувати URL автоматично мати відповідний API? Спочатку вважали, що generics.ListeCreateAPIView було найбільш ефективним, але занадто базовим, правда?
Себ

11

Що стосується надання різних серіалізаторів, чому ніхто не йде на підхід, який перевіряє метод HTTP? Він зрозуміліший IMO і не вимагає додаткових перевірок.

def get_serializer_class(self):
    if self.request.method == 'POST':
        return NewRackItemSerializer
    return RackItemSerializer

Кредити / джерело: https://github.com/encode/django-rest-framework/isissue/1563#issuecomment-42357718


12
У випадку, про який йдеться, що стосується використання іншого серіалізатора для listта retrieveдій, у вас є проблема, що обидва використовують GETметод. Ось чому рамка відпочинку django ViewSets використовує концепцію дій , яка схожа, але трохи відрізняється від відповідних методів http.
Håken Lid

8

На основі відповідей @gonz та @ user2734679 я створив цей невеликий пакет python, який надає цю функціональність у формі дочірнього класу ModelViewset. Ось як це працює.

from drf_custom_viewsets.viewsets.CustomSerializerViewSet
from myapp.serializers import DefaltSerializer, CustomSerializer1, CustomSerializer2

class MyViewSet(CustomSerializerViewSet):
    serializer_class = DefaultSerializer
    custom_serializer_classes = {
        'create':  CustomSerializer1,
        'update': CustomSerializer2,
    }

6
Краще використовувати міксин, який є загальним.
ямск

1

Хоча попереднє визначення декількох серіалізаторів тим чи іншим способом здається найбільш очевидно задокументованим способом, у FWIW існує альтернативний підхід, який спирається на інший задокументований код і який дозволяє передавати аргументи серіалізатору в міру його встановлення. Я думаю, це, ймовірно, буде більш вагомим, якщо вам потрібно буде генерувати логіку на основі різних факторів, таких як рівні адміністратора користувача, викликані дії, можливо, навіть атрибути екземпляра.

Перший фрагмент головоломки - це документація щодо динамічної модифікації серіалізатора в точці інстанції . Ця документація не пояснює, як викликати цей код із набору перегляду чи як змінити статус для читання тільки полів після того, як вони були започатковані - але це не дуже складно.

Другий фрагмент - метод get_serializer також задокументований - (лише трохи далі вниз на сторінку від get_serializer_class під "іншими методами"), тому на нього слід безпечно покластися (і джерело дуже просте, що, сподіваємось, означає менше шансів непередбачуваного побічні ефекти внаслідок модифікації). Перевірте джерело в GenericAPIView (ModelViewSet - і всі інші вбудовані в класи набору огляду, здається - успадковують від GenericAPIView, який визначає get_serializer.

Збираючи їх разом, ви можете зробити щось подібне:

У файлі серіалізаторів (для мене base_serializers.py):

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""

def __init__(self, *args, **kwargs):
    # Don't pass the 'fields' arg up to the superclass
    fields = kwargs.pop('fields', None)

    # Adding this next line to the documented example
    read_only_fields = kwargs.pop('read_only_fields', None)

    # Instantiate the superclass normally
    super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

    if fields is not None:
        # Drop any fields that are not specified in the `fields` argument.
        allowed = set(fields)
        existing = set(self.fields)
        for field_name in existing - allowed:
            self.fields.pop(field_name)

    # another bit we're adding to documented example, to take care of readonly fields 
    if read_only_fields is not None:
        for f in read_only_fields:
            try:
                self.fields[f].read_only = True
            exceptKeyError:
                #not in fields anyway
                pass

Тоді у вашому наборі даних ви можете зробити щось подібне:

class MyViewSet(viewsets.ModelViewSet):
    # ...permissions and all that stuff

    def get_serializer(self, *args, **kwargs):

        # the next line is taken from the source
        kwargs['context'] = self.get_serializer_context()

        # ... then whatever logic you want for this class e.g:
        if self.action == "list":
            rofs = ('field_a', 'field_b')
            fs = ('field_a', 'field_c')
        if self.action == retrieve”:
            rofs = ('field_a', 'field_c’, ‘field_d’)
            fs = ('field_a', 'field_b’)
        #  add all your further elses, elifs, drawing on info re the actions, 
        # the user, the instance, anything passed to the method to define your read only fields and fields ...
        #  and finally instantiate the specific class you want (or you could just
        # use get_serializer_class if you've defined it).  
        # Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
        kwargs['read_only_fields'] = rofs
        kwargs['fields'] = fs
        return MyDynamicSerializer(*args, **kwargs)

І це повинно бути! Використовуючи MyViewSet, слід інстанціювати ваш MyDynamicSerializer аргументами, які ви хочете - і припускаючи, що ваш серіалізатор успадковується від вашого DynamicFieldsModelSerializer, він повинен знати, що робити.

Можливо, варто згадати, що це може мати особливий сенс, якщо ви хочете адаптувати серіалізатор якимось іншим способом ... наприклад, робити такі речі, як взяти у списку read_only_exceptions і використовувати його для білого списку, а не для чорних полів (що я, як правило, роблю). Я також вважаю корисним встановити поля на порожній кортеж, якщо його не було пропущено, а потім просто зняти чек на None ... і я встановив визначення моїх полів на своїх успадкованих серіалізаторах на " всі ". Це означає, що поля, які не передаються при інстанціюванні серіалізатора, виживають випадково, і мені також не доведеться порівнювати виклик серіалізатора з спадковим визначенням класу серіалізаторів, щоб знати, що включено ... наприклад, в межах init програми DynamicFieldsModelSerializer:

# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....

NB Якщо я просто хотів два-три класи, які відображалися на різні дії та / або я не хотів особливої ​​динамічної поведінки серіалізатора, я міг би використати один із підходів, згаданих іншими тут, але я вважав, що це варто представити як альтернативу , особливо з огляду на інші його використання.

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