Struktura reszty Django, użyj różnych serializatorów w tym samym ModelViewSet


196

Chciałbym udostępnić dwa różne serializatory, a jednocześnie móc korzystać ze wszystkich udogodnień ModelViewSet:

  • Podczas przeglądania listy obiektów chciałbym, aby każdy obiekt miał adres URL, który przekierowuje do jego szczegółów, a każda inna relacja pojawia się przy użyciu __unicode __modelu docelowego;

przykład:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • Podczas przeglądania szczegółów obiektu chciałbym użyć wartości domyślnej HyperlinkedModelSerializer

przykład:

{
  "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/"
  ]
}

Udało mi się sprawić, aby wszystko to działało, jak chcę, w następujący sposób:

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.

Zasadniczo wykrywam, kiedy użytkownik żąda widoku listy lub widoku szczegółowego, i zmieniam serializer_classgo w zależności od moich potrzeb. Nie jestem jednak bardzo zadowolony z tego kodu, wygląda na brudny hack, a co najważniejsze, jeśli dwóch użytkowników zażąda listy i szczegółów w tym samym momencie?

Czy istnieje lepszy sposób na osiągnięcie tego, ModelViewSetsczy muszę się wycofać GenericAPIView?

EDYCJA:
Oto jak to zrobić przy użyciu niestandardowej bazy 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.
    }

jak ostatecznie to wdrożyliście? Używasz sposobu zaproponowanego przez user2734679 lub GenericAPIView?
andilabs

Zgodnie z sugestią użytkownika 2734679; Utworzyłem ogólny zestaw ViewSet, dodając słownik określający serializator dla każdej akcji i domyślny serializator, gdy nie jest określony
BlackBear

Mam podobny problem ( stackoverflow.com/questions/24809737/... ) i na razie się z nim skończyłem ( gist.github.com/andilab/a23a6370bd118bf5e858 ), ale nie jestem z niego bardzo zadowolony.
andilabs

1
Stworzyłem do tego ten mały pakiet. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

1
Metoda zastępowania pobierania jest OK.
gzerone

Odpowiedzi:


288

Zastąp swoją get_serializer_classmetodę. Ta metoda jest używana w mieszankach modelu w celu pobrania odpowiedniej klasy Serializer.

Zauważ, że istnieje również get_serializermetoda, która zwraca instancję poprawnego serializatora

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
To wspaniale, dziękuję! Zastąpiłem jednak klasę get_serializer_class
BlackBear

15
UWAGA: django rest swagger nie umieszcza parametru self.action, więc ta funkcja zgłosi wyjątek. Możesz użyć odpowiedzi gonza lub możesz użyćif hasattr(self, 'action') and self.action == 'list'
Tom Leys

Utwórz do tego mały pakiet pypi. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

Jak uzyskać pkżądany obiekt, jeśli akcja jest wykonywana retrieve?
Pranjal Mittal

Moja własna reakcja to Brak. Czy ktoś mógłby mi powiedzieć dlaczego?
Kakaji

86

Może się okazać, że ten mixin jest użyteczny, zastępuje metodę get_serializer_class i pozwala zadeklarować dykt, który odwzorowuje akcję i klasę serializatora lub przywraca normalne zachowanie.

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

Stworzyłem do tego ten mały pakiet. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

15

Ta odpowiedź jest taka sama jak odpowiedź zaakceptowana, ale wolę to zrobić w ten sposób.

Ogólne widoki

get_serializer_class(self):

Zwraca klasę, która powinna być używana dla serializatora. Domyślnie zwracany jest serializer_classatrybut.

Może zostać zastąpione w celu zapewnienia dynamicznego zachowania, takiego jak używanie różnych serializatorów do operacji odczytu i zapisu lub dostarczanie różnych serializatorów różnym typom użytkowników. atrybut 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)

Nie można go użyć, ponieważ mówi mi, że mój widok nie ma atrybutu „akcja”. Wygląda jak ProductIndex (generics.ListCreateAPIView). Czy to oznacza, że ​​absolutnie musisz przekazać zestawy widoków jako argument, czy istnieje sposób, aby to zrobić za pomocą ogólnych widoków interfejsu API?
Seb

1
późna odpowiedź na komentarz @Seb - być może ktoś może z tego skorzystać :) W przykładzie użyto ViewSets, a nie Views :)
fanny

W połączeniu z tym postem stackoverflow.com/questions/32589087/... , ViewSets wydają się być sposobem na uzyskanie większej kontroli nad różnymi widokami i generowanie adresu URL automatycznie, aby mieć spójny interfejs API? Początkowo uważano, że generics.ListeCreateAPIView był najbardziej wydajny, ale zbyt podstawowy, prawda?
Seb

11

Jeśli chodzi o zapewnianie różnych serializatorów, dlaczego nikt nie wybiera podejścia sprawdzającego metodę HTTP? Jest to wyraźniejsze IMO i nie wymaga żadnych dodatkowych kontroli.

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

Kredyty / źródło: https://github.com/encode/django-rest-framework/issues/1563#issuecomment-42357718


12
W omawianym przypadku, który dotyczy użycia innego serializatora listi retrieveakcji, masz problem z obiema GETmetodami. Dlatego właśnie django rest framework ViewSets używa pojęcia akcji , która jest podobna, ale nieco inna niż odpowiednie metody http.
Håken Lid

8

Na podstawie odpowiedzi @gonz i @ user2734679 stworzyłem ten mały pakiet python, który daje tę funkcjonalność w formie klasy potomnej ModelViewset . Oto jak to działa.

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
Lepiej użyj mixinu, który jest bardzo ogólny.
Iamsk

1

Chociaż wstępne zdefiniowanie wielu Serializatorów w taki czy inny sposób wydaje się być najbardziej udokumentowanym sposobem, FWIW jest alternatywnym podejściem, które opiera się na innym udokumentowanym kodzie i umożliwia przekazywanie argumentów do serializatora podczas jego tworzenia. Myślę, że byłoby bardziej opłacalne, gdybyś musiał wygenerować logikę na podstawie różnych czynników, takich jak poziomy administratora użytkowników, wywoływane działanie, a może nawet atrybuty instancji.

Pierwszym elementem układanki jest dokumentacja dynamicznej modyfikacji serializatora w momencie tworzenia wystąpienia . Ta dokumentacja nie wyjaśnia, jak wywołać ten kod z zestawu widoków ani jak zmodyfikować status pola tylko do odczytu po ich zainicjowaniu - ale to nie jest bardzo trudne.

Drugi kawałek - metoda get_serializer jest również udokumentowany - (nieco dalej w dół strony od get_serializer_class w „innych metodach”), więc warto na nim polegać (a źródło jest bardzo proste, co, mam nadzieję, oznacza mniejszą szansę na niezamierzone skutki uboczne wynikające z modyfikacji). Sprawdź źródło w GenericAPIView (ModelViewSet - i wydaje się, że wszystkie inne wbudowane klasy widoków - dziedziczą po GenericAPIView, który definiuje get_serializer.

Łącząc te dwie rzeczy, możesz zrobić coś takiego:

W pliku serializerów (dla mnie 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

Następnie w swoim widoku możesz zrobić coś takiego:

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)

I to powinno być to! Korzystanie z MyViewSet powinno teraz utworzyć instancję MyDynamicSerializer z argumentami, które chcesz - i zakładając, że twój serializator dziedziczy po DynamicFieldsModelSerializer, powinien wiedzieć, co robić.

Być może warto wspomnieć, że może to mieć szczególny sens, jeśli chcesz dostosować serializator na inne sposoby ... np. Zrobić rzeczy takie jak pobranie listy read_only_exceptions i użycie jej do białej listy zamiast pól czarnej listy (co zwykle robię). Przydaje mi się również ustawienie pól na pustą krotkę, jeśli nie została ona przekazana, a następnie usunięcie zaznaczenia opcji Brak ... i ustawiłem definicje pól w dziedziczących serializatorach na „ wszystkie ”. Oznacza to, że żadne pola, które nie zostaną przekazane podczas tworzenia instancji serializatora, przetrwają przypadkowo, a ja nie muszę porównywać wywołania serializatora z dziedziczącą definicją klasy serializatora, aby wiedzieć, co zostało uwzględnione ... np. W trakcie inicjowania DynamicFieldsModelSerializer:

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

NB: Gdybym chciał tylko dwie lub trzy klasy, które byłyby przypisane do różnych akcji i / lub nie chciałbym żadnego specjalnie dynamicznego zachowania serializatora, mógłbym użyć jednego z podejść wymienionych tutaj przez innych, ale pomyślałem, że warto to przedstawić jako alternatywę , szczególnie biorąc pod uwagę jego inne zastosowania.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.