Django reste framework, utilisez différents sérialiseurs dans le même ModelViewSet

197

Je souhaite fournir deux sérialiseurs différents tout en pouvant bénéficier de toutes les fonctionnalités de ModelViewSet:

  • Lors de la visualisation d'une liste d'objets, je voudrais que chaque objet ait une URL qui redirige vers ses détails et que toutes les autres relations apparaissent en utilisant __unicode __le modèle cible;

exemple:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • Lors de la visualisation des détails d'un objet, je souhaite utiliser la valeur par défaut HyperlinkedModelSerializer

exemple:

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

J'ai réussi à faire fonctionner tout cela comme je le souhaite de la manière suivante:

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.

Fondamentalement, je détecte quand l'utilisateur demande une vue de liste ou une vue détaillée et change serializer_classpour répondre à mes besoins. Je ne suis pas vraiment satisfait de ce code, cela ressemble à un sale hack et, surtout, que se passe-t-il si deux utilisateurs demandent une liste et un détail au même moment?

Existe-t-il un meilleur moyen d'y parvenir en utilisant ModelViewSetsou dois-je utiliser de nouveau GenericAPIView?

EDIT:
Voici comment le faire en utilisant une base personnalisée 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.
    }
Ours noir
la source
comment l'avez-vous mis en œuvre finalement? En utilisant la voie proposée par user2734679 ou en utilisant GenericAPIView?
andilabs
Comme suggéré par user2734679; J'ai créé un ViewSet générique en ajoutant un dictionnaire pour spécifier le sérialiseur pour chaque action et un sérialiseur par défaut lorsqu'il n'est pas spécifié
BlackBear
J'ai un problème similaire ( stackoverflow.com/questions/24809737/… ) et pour l'instant terminé avec lui ( gist.github.com/andilab/a23a6370bd118bf5e858 ), mais je n'en suis pas très satisfait.
andilabs
1
Créé ce petit paquet pour cela. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
1
La méthode de récupération de remplacement est OK.
gzerone

Réponses:

289

Remplacez votre get_serializer_classméthode. Cette méthode est utilisée dans vos mixins de modèle pour récupérer la classe Serializer appropriée.

Notez qu'il existe également une get_serializerméthode qui renvoie une instance du sérialiseur correct

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
la source
1
C'est super, merci! J'ai cependant remplacé get_serializer_class
BlackBear
15
AVERTISSEMENT: django rest swagger ne place pas de paramètre self.action, donc cette fonction lèvera une exception. Vous pourriez utiliser la réponse de gonz ou vous pourriez utiliserif hasattr(self, 'action') and self.action == 'list'
Tom Leys
Créez un petit package pypi pour cela. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
Comment obtenir l' pkobjet de demande, si l'action est retrieve?
Pranjal Mittal
Mon self.action est Aucun. Quelqu'un pourrait-il me dire pourquoi?
Kakaji
87

Vous pouvez trouver ce mixin utile, il remplace la méthode get_serializer_class et vous permet de déclarer un dict qui mappe l'action et la classe de sérialiseur ou de repli sur le comportement habituel.

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
la source
Créé ce petit paquet pour cela. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
15

Cette réponse est la même que la réponse acceptée mais je préfère faire de cette manière.

Vues génériques

get_serializer_class(self):

Renvoie la classe qui doit être utilisée pour le sérialiseur. Par défaut, il renvoie l' serializer_classattribut.

Peut être remplacé pour fournir un comportement dynamique, comme l'utilisation de différents sérialiseurs pour les opérations de lecture et d'écriture ou la fourniture de différents sérialiseurs aux différents types d'utilisateurs. l'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
la source
Impossible de l'utiliser car il me dit que ma vue n'a pas d'attribut "action". Il ressemble à ProductIndex (generics.ListCreateAPIView). Cela signifie-t-il que vous devez absolument transmettre des ensembles de vues en argument ou existe-t-il un moyen de le faire en utilisant les vues API génériques?
Seb
1
une réponse tardive au commentaire @Seb - peut-être que quelqu'un peut en profiter :) L'exemple utilise ViewSets, pas Views :)
fanny
Alors combiné avec ce post stackoverflow.com/questions/32589087/… , ViewSets semble être la voie à suivre pour avoir plus de contrôle sur les différentes vues et générer automatiquement une URL pour avoir une API cohérente? Initialement pensé que generics.ListeCreateAPIView était le plus efficace, mais trop basique, non?
Seb le
11

En ce qui concerne la fourniture de différents sérialiseurs, pourquoi personne n'utilise l'approche qui vérifie la méthode HTTP? Il est plus clair IMO et ne nécessite aucun contrôle supplémentaire.

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

Crédits / source: https://github.com/encode/django-rest-framework/issues/1563#issuecomment-42357718

Luca Bezerra
la source
12
Pour le cas en question, qui consiste à utiliser un sérialiseur différent pour listet des retrieveactions, vous avez le problème que les deux utilisent la GETméthode. C'est pourquoi django rest framework ViewSets utilise le concept d' actions , qui est similaire, mais légèrement différent des méthodes http correspondantes.
Håken Lid
8

Sur la base des réponses @gonz et @ user2734679, j'ai créé ce petit package python qui donne cette fonctionnalité sous la forme d'une classe enfant de ModelViewset. Voici comment cela fonctionne.

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
la source
6
Il vaut mieux utiliser mixin qui est beaucoup plus générique.
iamsk
1

Bien que prédéfinir plusieurs sérialiseurs d'une manière ou d'une autre semble être la manière la plus clairement documentée , il existe une approche alternative qui s'appuie sur un autre code documenté et qui permet de passer des arguments au sérialiseur lors de son instanciation. Je pense que cela aurait probablement tendance à être plus utile si vous deviez générer une logique basée sur divers facteurs, tels que les niveaux d'administrateur des utilisateurs, l'action appelée, peut-être même les attributs de l'instance.

La première pièce du puzzle est la documentation sur la modification dynamique d'un sérialiseur au point d'instanciation . Cette documentation n'explique pas comment appeler ce code à partir d'un ensemble de vues ou comment modifier l'état en lecture seule des champs après leur lancement - mais ce n'est pas très difficile.

La deuxième pièce - le méthode get_serializer est également documentée - (juste un peu plus loin dans la page de get_serializer_class sous `` autres méthodes ''), il devrait donc être sûr de s'appuyer sur (et la source est très simple, ce qui, espérons-le, signifie moins de chance de effets secondaires résultant d'une modification). Vérifiez la source sous GenericAPIView (le ModelViewSet - et toutes les autres classes d'ensemble de vues intégrées semble-t-il - hérite du GenericAPIView qui définit get_serializer.

En mettant les deux ensemble, vous pourriez faire quelque chose comme ceci:

Dans un fichier de sérialiseurs (pour moi 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

Ensuite, dans votre ensemble de vues, vous pouvez faire quelque chose comme ceci:

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)

Et ça devrait être ça! L'utilisation de MyViewSet devrait maintenant instancier votre MyDynamicSerializer avec les arguments que vous souhaitez - et en supposant que votre sérialiseur hérite de votre DynamicFieldsModelSerializer, il devrait savoir exactement quoi faire.

Cela vaut peut-être la peine de mentionner que cela peut avoir un sens particulier si vous voulez adapter le sérialiseur d'une autre manière… par exemple pour faire des choses comme prendre une liste read_only_exceptions et l'utiliser pour ajouter des champs à une liste blanche plutôt qu'à une liste noire (ce que j'ai tendance à faire). Je trouve également utile de définir les champs sur un tuple vide si ce n'est pas passé, puis de supprimer simplement la vérification de Aucun ... et j'ai défini mes définitions de champs sur mes sérialiseurs héritiers sur « tous ». Cela signifie qu'aucun champ qui n'est pas passé lors de l'instanciation du sérialiseur ne survit par accident et je n'ai pas non plus à comparer l'invocation du sérialiseur avec la définition de la classe du sérialiseur héréditaire pour savoir ce qui a été inclus ... par exemple dans l' init héréditaire du DynamicFieldsModelSerializer:

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

NB Si je voulais juste deux ou trois classes mappées sur des actions distinctes et / ou que je ne voulais pas de comportement de sérialiseur spécialement dynamique, je pourrais bien utiliser l'une des approches mentionnées par d'autres ici, mais j'ai pensé que cela valait la peine d'être présenté comme une alternative , compte tenu notamment de ses autres utilisations.

user1936977
la source