Django rest framework, use diferentes serializadores en el mismo ModelViewSet

196

Me gustaría proporcionar dos serializadores diferentes y, sin embargo, poder beneficiarme de todas las facilidades de ModelViewSet:

  • Al ver una lista de objetos, me gustaría que cada objeto tenga una URL que redirija a sus detalles y que cualquier otra relación aparezca usando __unicode __el modelo de destino;

ejemplo:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • Al ver los detalles de un objeto, me gustaría usar el valor predeterminado HyperlinkedModelSerializer

ejemplo:

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

Logré hacer que todo esto funcione como lo deseo de la siguiente manera:

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.

Básicamente, detecto cuando el usuario solicita una vista de lista o una vista detallada y cambio serializer_classpara satisfacer mis necesidades. Sin embargo, no estoy realmente satisfecho con este código, parece un truco sucio y, lo más importante, ¿qué pasa si dos usuarios solicitan una lista y un detalle en el mismo momento?

¿Hay una mejor manera de lograr esto usando ModelViewSetso tengo que recurrir al uso GenericAPIView?

EDITAR:
Aquí se explica cómo hacerlo utilizando una base personalizada 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.
    }
Oso negro
fuente
¿Cómo lo implementaste finalmente? ¿Usando la forma propuesta por el usuario2734679 o usando GenericAPIView?
andilabs
Según lo sugerido por el usuario2734679; Creé un ViewSet genérico agregando un diccionario para especificar el serializador para cada acción y un serializador predeterminado cuando no se especifica
BlackBear
Tengo un problema similar ( stackoverflow.com/questions/24809737/… ) y por ahora terminé con él ( gist.github.com/andilab/a23a6370bd118bf5e858 ), pero no estoy muy satisfecho con él.
andilabs
1
Creé este pequeño paquete para esto. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
1
El método de recuperación de anulación está bien.
gzerone

Respuestas:

288

Anula tu get_serializer_classmétodo. Este método se utiliza en los mixins de su modelo para recuperar la clase de serializador adecuada.

Tenga en cuenta que también hay un get_serializermétodo que devuelve una instancia del serializador correcto

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.                
usuario133688
fuente
1
Esto es genial, gracias! Sin embargo, he anulado get_serializer_class
BlackBear
15
ADVERTENCIA: django rest swagger no coloca un parámetro self.action, por lo que esta función generará una excepción. Puede usar la respuesta de gonz o puede usarif hasattr(self, 'action') and self.action == 'list'
Tom Leys
Crea un pequeño paquete pypi para esto. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
¿Cómo obtenemos el pkobjeto solicitado, si la acción es retrieve?
Pranjal Mittal
Mi autoacción es Ninguno. ¿Alguien podría decirme por qué?
Kakaji
86

Puede encontrar esta combinación útil, anula el método get_serializer_class y le permite declarar un dict que asigna acción y clase de serializador o respaldo al comportamiento habitual.

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
fuente
Creé este pequeño paquete para esto. github.com/Darwesh27/drf-custom-viewsets
Adil Malik
15

Esta respuesta es la misma que la respuesta aceptada, pero prefiero hacerlo de esta manera.

Vistas genéricas

get_serializer_class(self):

Devuelve la clase que se debe usar para el serializador. El valor predeterminado es devolver el serializer_classatributo.

Puede anularse para proporcionar un comportamiento dinámico, como el uso de diferentes serializadores para operaciones de lectura y escritura o proporcionar diferentes serializadores a los diferentes tipos de usuarios. El atributo 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
fuente
No se puede usar porque me dice que mi vista no tiene atributo "acción". Parece ProductIndex (generics.ListCreateAPIView). ¿Significa que es absolutamente necesario pasar vistas como argumento o hay una manera de hacerlo usando las vistas genéricas de API?
Seb
1
una respuesta tardía al comentario de @Seb: tal vez alguien pueda beneficiarse de eso :) El ejemplo usa ViewSets, no Vistas :)
fanny
Entonces, combinado con esta publicación stackoverflow.com/questions/32589087/… , ¿ViewSets parece ser el camino a seguir para tener más control sobre las diferentes vistas y generar URL automáticamente para tener una API consistente? Originalmente pensé que genéricos.ListeCreateAPIView era el más eficiente, pero demasiado básico ¿verdad?
Seb
10

Con respecto a proporcionar diferentes serializadores, ¿por qué nadie va por el enfoque que verifica el método HTTP? Es más claro IMO y no requiere controles adicionales.

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

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

Luca Bezerra
fuente
12
Para el caso en cuestión, que se trata de usar un serializador listy retrieveacciones diferentes, tiene el problema de que ambos usan el GETmétodo. Es por eso que django rest framework ViewSets utiliza el concepto de acciones , que son similares, pero ligeramente diferentes a los métodos http correspondientes.
Tapa Håken
8

Basado en las respuestas @gonz y @ user2734679, he creado este pequeño paquete de Python que brinda esta funcionalidad en forma de una clase secundaria de ModelViewset. Así es como funciona.

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
fuente
66
Es mejor usar mixin que mucho genérico.
iamsk
1

Aunque predefinir múltiples serializadores de una forma u otra parece ser la forma más obviamente documentada , FWIW existe un enfoque alternativo que se basa en otro código documentado y que permite pasar argumentos al serializador a medida que se instancia. Creo que probablemente valdría más la pena si necesitara generar lógica basada en varios factores, como los niveles de administración del usuario, la acción que se llama, tal vez incluso los atributos de la instancia.

La primera pieza del rompecabezas es la documentación sobre la modificación dinámica de un serializador en el punto de instanciación . Esa documentación no explica cómo llamar a este código desde un conjunto de vistas o cómo modificar el estado de solo lectura de los campos después de haber sido iniciados, pero eso no es muy difícil.

La segunda parte, el método get_serializer también está documentado, (un poco más abajo en la página de get_serializer_class en 'otros métodos'), por lo que debería ser seguro confiar en él (y la fuente es muy simple, lo que con suerte significa menos posibilidades de que no sea intencional efectos secundarios resultantes de la modificación). Verifique la fuente en GenericAPIView (ModelViewSet, y todas las demás clases de viewset incorporadas que parece), heredan de GenericAPIView que define get_serializer.

Poniendo los dos juntos, podría hacer algo como esto:

En un archivo de serializadores (para mí 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

Luego, en su conjunto de vistas, puede hacer algo como esto:

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)

¡Y eso debería ser! El uso de MyViewSet ahora debería instanciar su MyDynamicSerializer con los argumentos que desee, y suponiendo que su serializador herede de su DynamicFieldsModelSerializer, debe saber exactamente qué hacer.

Quizás valga la pena mencionar que puede tener un sentido especial si desea adaptar el serializador de alguna otra manera ... por ejemplo, para hacer cosas como tomar en una lista read_only_exceptions y usarla en la lista blanca en lugar de los campos de la lista negra (lo que suelo hacer). También me parece útil establecer los campos en una tupla vacía si no se pasa y luego simplemente elimino la marca de Ninguno ... y configuro mis definiciones de campos en mis Serializadores heredados a ' todos '. Esto significa que ningún campo que no se pasa al crear instancias del serializador sobrevive por accidente y tampoco tengo que comparar la invocación del serializador con la definición de clase del serializador heredado para saber qué se ha incluido ... por ejemplo, dentro del inicio de DynamicFieldsModelSerializer:

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

Nota: si solo quisiera dos o tres clases que se asignaran a acciones distintas y / o no quisiera ningún comportamiento serializador especialmente dinámico, podría utilizar uno de los enfoques mencionados por otros aquí, pero pensé que valía la pena presentarlo como una alternativa , particularmente dados sus otros usos.

usuario1936977
fuente