Verwenden Sie im Django Rest Framework verschiedene Serializer im selben ModelViewSet

195

Ich möchte zwei verschiedene Serializer bereitstellen und dennoch von allen Möglichkeiten profitieren ModelViewSet:

  • Wenn ich eine Liste von Objekten ansehe, möchte ich, dass jedes Objekt eine URL hat, die zu seinen Details umleitet, und dass jede andere Beziehung unter Verwendung __unicode __des Zielmodells angezeigt wird.

Beispiel:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • Beim Anzeigen der Details eines Objekts möchte ich die Standardeinstellung verwenden HyperlinkedModelSerializer

Beispiel:

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

Ich habe es geschafft, all diese Arbeit so zu machen, wie ich es mir wünsche:

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.

Grundsätzlich erkenne ich, wann der Benutzer eine Listenansicht oder eine Detailansicht anfordert, und ändere serializer_classsie entsprechend meinen Anforderungen. Ich bin jedoch nicht wirklich zufrieden mit diesem Code, er sieht aus wie ein schmutziger Hack und vor allem, was ist, wenn zwei Benutzer gleichzeitig eine Liste und ein Detail anfordern?

Gibt es einen besseren Weg, um dies mit zu erreichen, ModelViewSetsoder muss ich mit zurückgreifen GenericAPIView?

BEARBEITEN:
So geht's mit einer benutzerdefinierten Basis 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.
    }
Schwarzbär
quelle
Wie haben Sie es endgültig umgesetzt? Verwenden Sie den von user2734679 vorgeschlagenen Weg oder verwenden Sie GenericAPIView?
Andilabs
Wie von user2734679 vorgeschlagen; Ich habe ein generisches ViewSet erstellt, das ein Wörterbuch hinzufügt, um den Serializer für jede Aktion anzugeben, und einen Standard-Serializer, wenn nicht angegeben
BlackBear
Ich habe ein ähnliches Problem ( stackoverflow.com/questions/24809737/… ) und habe es vorerst beendet ( gist.github.com/andilab/a23a6370bd118bf5e858 ), bin aber nicht sehr zufrieden damit.
Andilabs
1
Erstellt dieses kleine Paket dafür. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
1
Die Methode zum Überschreiben des Abrufs ist in Ordnung.
Gzerone

Antworten:

287

Überschreiben Sie Ihre get_serializer_classMethode. Diese Methode wird in Ihren Modellmixins verwendet, um die richtige Serializer-Klasse abzurufen.

Beachten Sie, dass es auch eine get_serializerMethode gibt, die eine Instanz des richtigen Serializers zurückgibt

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
quelle
1
Das ist großartig, danke! Ich habe get_serializer_class allerdings überschrieben
BlackBear
15
WARNUNG: django rest swagger platziert keinen self.action-Parameter, daher löst diese Funktion eine Ausnahme aus. Sie könnten Gonz 'Antwort verwenden oder Sie könnten verwendenif hasattr(self, 'action') and self.action == 'list'
Tom Leys
Erstellen Sie dazu ein kleines Pypi-Paket. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
Wie erhalten wir das pkangeforderte Objekt, wenn die Aktion ausgeführt wird retrieve?
Pranjal Mittal
Meine Selbsttätigkeit ist Keine. Könnte mir jemand sagen warum?
Kakaji
86

Dieses Mixin ist möglicherweise hilfreich. Es überschreibt die Methode get_serializer_class und ermöglicht es Ihnen, ein Diktat zu deklarieren, das die Aktion und die Serializer-Klasse oder den Fallback dem üblichen Verhalten zuordnet.

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
quelle
Erstellt dieses kleine Paket dafür. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
15

Diese Antwort ist die gleiche wie die akzeptierte Antwort, aber ich bevorzuge es auf diese Weise.

Allgemeine Ansichten

get_serializer_class(self):

Gibt die Klasse zurück, die für den Serializer verwendet werden soll. Standardmäßig wird das serializer_classAttribut zurückgegeben.

Kann überschrieben werden, um ein dynamisches Verhalten bereitzustellen, z. B. die Verwendung verschiedener Serializer für Lese- und Schreibvorgänge oder die Bereitstellung verschiedener Serializer für die verschiedenen Benutzertypen. das Attribut 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)
Mohammad Masoumi
quelle
Kann es nicht verwenden, da es mir sagt, dass meine Ansicht kein Attribut "Aktion" hat. Es sieht aus wie ProductIndex (generics.ListCreateAPIView). Bedeutet dies, dass Sie unbedingt Ansichtssätze als Argument übergeben müssen, oder gibt es eine Möglichkeit, dies mithilfe der generischen API-Ansichten zu tun?
Seb
1
eine späte Antwort auf @Seb Kommentar - vielleicht kann jemand davon profitieren :) Das Beispiel verwendet ViewSets, nicht Views :)
Fanny
In Kombination mit diesem Beitrag stackoverflow.com/questions/32589087/… scheinen ViewSets der Weg zu sein, um mehr Kontrolle über die verschiedenen Ansichten zu haben und automatisch eine URL zu generieren, um eine konsistente API zu erhalten. Ursprünglich dachte, dass generics.ListeCreateAPIView das effizienteste, aber zu grundlegende ist, oder?
Seb
10

Warum entscheidet sich niemand für die Bereitstellung verschiedener Serializer für den Ansatz, der die HTTP-Methode überprüft? Es ist klarer IMO und erfordert keine zusätzlichen Überprüfungen.

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

Credits / Quelle: https://github.com/encode/django-rest-framework/issues/1563#issuecomment-42357718

Luca Bezerra
quelle
12
Für den fraglichen Fall, bei dem es darum geht, einen anderen Serializer für listund retrieveAktionen zu verwenden, besteht das Problem, dass beide GETMethoden verwenden. Aus diesem Grund verwendet das Django Rest Framework ViewSets das Konzept von Aktionen , das ähnlich ist, sich jedoch geringfügig von den entsprechenden http-Methoden unterscheidet.
Håken Deckel
8

Basierend auf den Antworten von @gonz und @ user2734679 habe ich dieses kleine Python-Paket erstellt , das diese Funktionalität in Form einer untergeordneten Klasse von ModelViewset bereitstellt . So funktioniert es.

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,
    }
Adil Malik
quelle
6
Es ist besser, Mixin zu verwenden, das viel generisch ist.
Iamsk
1

Obwohl die Vordefinition mehrerer Serializer auf die eine oder andere Weise am offensichtlichsten dokumentiert zu sein scheint , gibt es bei FWIW einen alternativen Ansatz, der auf anderem dokumentiertem Code basiert und die Übergabe von Argumenten an den Serializer ermöglicht, wenn dieser instanziiert wird. Ich denke, es wäre wahrscheinlich lohnender, wenn Sie Logik generieren müssten, die auf verschiedenen Faktoren basiert, wie z. B. Benutzeradministrationsebenen, der aufgerufenen Aktion oder möglicherweise sogar Attributen der Instanz.

Der erste Teil des Puzzles ist die Dokumentation zum dynamischen Ändern eines Serialisierers zum Zeitpunkt der Instanziierung . In dieser Dokumentation wird nicht erläutert, wie dieser Code aus einem Viewset aufgerufen oder der schreibgeschützte Status von Feldern nach deren Initiierung geändert wird. Dies ist jedoch nicht sehr schwierig.

Das zweite Stück - die get_serializer-Methode ist ebenfalls dokumentiert - (etwas weiter unten auf der Seite von get_serializer_class unter "andere Methoden") sollte daher sicher sein (und die Quelle ist sehr einfach, was hoffentlich eine geringere Wahrscheinlichkeit für unbeabsichtigte Ereignisse bedeutet Nebenwirkungen infolge von Veränderungen). Überprüfen Sie die Quelle unter GenericAPIView (das ModelViewSet - und alle anderen integrierten Viewset-Klassen, wie es scheint - erben von der GenericAPIView, die get_serializer definiert.

Wenn Sie die beiden zusammenfügen, können Sie so etwas tun:

In einer Serializer-Datei (für mich 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

Dann könnten Sie in Ihrem Viewset Folgendes tun:

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)

Und das sollte es sein! Die Verwendung von MyViewSet sollte jetzt Ihren MyDynamicSerializer mit den gewünschten Argumenten instanziieren - und vorausgesetzt, Ihr Serializer erbt von Ihrem DynamicFieldsModelSerializer, sollte er genau wissen, was zu tun ist.

Vielleicht ist es erwähnenswert, dass es besonders sinnvoll sein kann, wenn Sie den Serializer auf andere Weise anpassen möchten… z. B. um beispielsweise eine read_only_exceptions-Liste aufzunehmen und sie eher zur Whitelist als zu Blacklist-Feldern zu verwenden (was ich normalerweise tue). Ich finde es auch nützlich, die Felder auf ein leeres Tupel zu setzen, wenn es nicht übergeben wird, und dann einfach die Prüfung für Keine zu entfernen ... und meine Felddefinitionen für meine ererbenden Serializer auf " Alle " zu setzen. Dies bedeutet, dass keine Felder, die beim Instanziieren des Serializers nicht übergeben werden, versehentlich überleben, und ich muss auch den Serializer-Aufruf nicht mit der ererbenden Serializer-Klassendefinition vergleichen, um zu wissen, was enthalten ist ... z Init des DynamicFieldsModelSerializer:

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

NB Wenn ich nur zwei oder drei Klassen haben wollte, die unterschiedlichen Aktionen zugeordnet sind, und / oder ich kein besonders dynamisches Serializer-Verhalten wollte, könnte ich einen der von anderen hier erwähnten Ansätze verwenden, aber ich fand, dass dies eine Alternative wert ist , insbesondere angesichts seiner anderen Verwendungen.

user1936977
quelle