Django rest framework, использовать разные сериализаторы в одном ModelViewSet

197

Я хотел бы предоставить два разных сериализатора и при этом иметь возможность пользоваться всеми возможностями 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?

РЕДАКТИРОВАТЬ:
Вот как это сделать, используя пользовательскую базу 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
По предложению user2734679; Я создал общий ViewSet, добавив словарь для указания сериализатора для каждого действия и сериализатор по умолчанию, если он не указан
BlackBear
У меня есть похожая проблема ( stackoverflow.com/questions/24809737/… ) и на данный момент с ней покончено ( gist.github.com/andilab/a23a6370bd118bf5e858 ), но я не очень доволен этим.
andilabs
1
Создал этот маленький пакет для этого. github.com/Darwesh27/drf-custom-viewsets
Адиль Малик,
1
Переопределить метод получения в порядке.
gzerone

Ответы:

289

Переопределите свой get_serializer_classметод. Этот метод используется в ваших модельных миксинах для получения правильного класса 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.                
user133688
источник
1
Это здорово, спасибо! Я переопределил get_serializer_class, хотя
BlackBear
15
ВНИМАНИЕ: django rest swagger не помещает параметр self.action, поэтому эта функция будет выдавать исключение. Вы можете использовать ответ Гонца, или вы можете использоватьif hasattr(self, 'action') and self.action == 'list'
Том Лейс
Создайте для этого небольшой пакет pypi. github.com/Darwesh27/drf-custom-viewsets
Адиль Малик,
Как получить pkзапрошенный объект, если действие выполнено retrieve?
Пранджал Миттал
Мое личное действие - Нет. Может кто-нибудь сказать мне, почему?
Какаджи
87

Вы можете найти этот миксин полезным, он переопределяет метод get_serializer_class и позволяет вам объявить dict, который отображает действие и класс сериализатора или отступает к обычному поведению.

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()
Gonz
источник
Создал этот маленький пакет для этого. 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?
Себ
1
поздний ответ на комментарий @Seb - возможно, кто-то может извлечь из этого выгоду :) В примере используются ViewSets, а не Views :)
fanny
Таким образом, в сочетании с этим сообщением stackoverflow.com/questions/32589087/… ViewSets, кажется, способ получить больший контроль над различными представлениями и автоматически генерировать URL, чтобы иметь согласованный API? Первоначально считалось, что generics.ListeCreateAPIView является наиболее эффективным, но слишком основным правом?
Себ
11

Что касается предоставления различных сериализаторов, почему никто не собирается использовать подход, который проверяет метод HTTP? Это яснее ИМО и не требует дополнительных проверок.

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

Кредиты / источник: https://github.com/encode/django-rest-framework/issues/1563#issuecomment-42357718

Лука Безерра
источник
12
В рассматриваемом случае, когда речь идет об использовании другого сериализатора для listи retrieveдействий, у вас есть проблема, которая использует GETметод. Вот почему django rest framework 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 и использовать его в белый список, а не в черный список (что я склонен делать). Я также считаю полезным установить поля равными пустому кортежу, если он не прошел, а затем просто убрать проверку «Нет» ... и я установил определения полей в своих наследующих сериализаторах как « все ». Это означает, что никакие поля, которые не передаются при создании экземпляра сериализатора, не выживают случайно, и мне также не нужно сравнивать вызов сериализатора с определением класса наследующего сериализатора, чтобы узнать, что было включено ... например, в рамках инициализации DynamicFieldsModelSerializer:

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

NB. Если бы я просто хотел, чтобы два или три класса отображались на разные действия, и / или я не хотел какого-либо специально динамического поведения сериализатора, я вполне мог бы использовать один из подходов, упомянутых здесь другими, но я подумал, что это стоит представить в качестве альтернативы. , особенно учитывая его другое использование.

user1936977
источник