No framework de resto do Django, use serializadores diferentes no mesmo ModelViewSet


195

Gostaria de fornecer dois serializadores diferentes e, ainda assim, poder me beneficiar de todas as facilidades de ModelViewSet:

  • Ao visualizar uma lista de objetos, gostaria que cada objeto tivesse um URL que redirecionasse para seus detalhes e todas as outras relações fossem exibidas usando __unicode __o modelo de destino;

exemplo:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • Ao visualizar os detalhes de um objeto, eu gostaria de usar o padrão HyperlinkedModelSerializer

exemplo:

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

Consegui fazer tudo isso funcionar da maneira que desejo:

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.

Basicamente, detecto quando o usuário está solicitando uma exibição de lista ou uma exibição detalhada e altero serializer_classpara atender às minhas necessidades. No entanto, não estou realmente satisfeito com esse código, parece um hack sujo e, o mais importante, e se dois usuários solicitarem uma lista e um detalhe no mesmo momento?

Existe uma maneira melhor de conseguir isso usando ModelViewSetsou eu tenho que voltar a usar GenericAPIView?

EDIT:
Veja como fazê-lo usando uma 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.
    }

como você implementou isso finalmente? Usando a maneira proposta pelo usuário2734679 ou usando o GenericAPIView?
18714 Andilabs

Conforme sugerido pelo usuário2734679; Eu criei um ViewSet genérico adicionando um dicionário para especificar o serializador para cada ação e um serializador padrão quando não especificado
BlackBear

Eu tenho um problema semelhante ( stackoverflow.com/questions/24809737/… ) e por enquanto terminou com ele ( gist.github.com/andilab/a23a6370bd118bf5e858 ), mas não estou muito satisfeito com isso.
18714 andilabs

1
Criado este pequeno pacote para isso. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

1
O método de recuperação de substituição está OK.
gzerone

Respostas:


287

Substitua seu get_serializer_classmétodo. Este método é usado em seus mixins de modelo para recuperar a classe Serializer adequada.

Observe que também existe um get_serializermétodo que retorna uma instância do serializador correto

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
Isso é ótimo, obrigado! Eu substitui o get_serializer_class embora
BlackBear

15
AVISO: o django rest swagger não coloca um parâmetro self.action, portanto, essa função gera uma exceção. Você pode usar a resposta de gonz ou você pode usarif hasattr(self, 'action') and self.action == 'list'
Tom Leys

Crie um pequeno pacote pypi para isso. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

Como obtemos o pkobjeto of solicitado, se a ação é retrieve?
Pranjal Mittal

Minha ação pessoal é Nenhuma. Alguém poderia me dizer por que?
Kakaji 16/05/19

86

Você pode achar útil esse mix, ele substitui o método get_serializer_class e permite declarar um ditado que mapeia a ação e a classe do serializador ou o fallback para o comportamento usual.

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

Criado este pequeno pacote para isso. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

15

Esta resposta é a mesma que a resposta aceita, mas prefiro fazer dessa maneira.

Visualizações genéricas

get_serializer_class(self):

Retorna a classe que deve ser usada para o serializador. O padrão é retornar oserializer_class atributo.

Pode ser substituído para fornecer um comportamento dinâmico, como o uso de serializadores diferentes para operações de leitura e gravação ou o fornecimento de serializadores diferentes para os diferentes tipos de usuários. o 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)

Não é possível usá-lo porque me diz que minha visão não tem atributo "ação". Parece ProductIndex (generics.ListCreateAPIView). Isso significa que você absolutamente precisa passar conjuntos de visualizações como argumento ou existe uma maneira de fazer isso usando as visualizações da API de genéricos?
Seb

1
uma resposta tardia ao comentário @Seb - talvez alguém possa lucrar com isso :) O exemplo usa ViewSets, não Views :)
fanny

Então, combinado com este post stackoverflow.com/questions/32589087/… , os ViewSets parecem ser o caminho para ter mais controle sobre as diferentes visualizações e gerar URL automaticamente para ter uma API consistente? Originalmente pensado que os genéricos.ListeCreateAPIView eram os mais eficientes, mas básicos demais, certo?
Seb

10

Em relação ao fornecimento de serializadores diferentes, por que ninguém está adotando a abordagem que verifica o método HTTP? É IMO mais claro e não requer verificações extras.

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

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


12
Para o caso em questão, que consiste em usar um serializador liste retrieveações diferentes para , você tem o problema de que ambos usam o GETmétodo. É por isso que o django rest framework ViewSets usa o conceito de ações , que são semelhantes, mas um pouco diferentes dos métodos http correspondentes.
HAKEN tampa

8

Com base nas respostas @gonz e @ user2734679, criei este pequeno pacote python que fornece essa funcionalidade na forma de uma classe filho do ModelViewset. Aqui está como isso 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,
    }

6
É melhor usar mixin que muito genérico.
Iamsk # 12/16

1

Embora pré-definir vários serializadores de uma maneira ou de outra pareça ser o mais obviamente documentado , o FWIW existe uma abordagem alternativa que se baseia em outro código documentado e que permite passar argumentos para o serializador à medida que é instanciado. Eu acho que provavelmente tenderia a valer mais a pena se você precisasse gerar lógica com base em vários fatores, como níveis de administrador do usuário, a ação que está sendo chamada, talvez até atributos da instância.

A primeira peça do quebra-cabeça é a documentação sobre a modificação dinâmica de um serializador no ponto de instanciação . Essa documentação não explica como chamar esse código de um conjunto de visualizações ou como modificar o status somente leitura dos campos depois que eles foram iniciados - mas isso não é muito difícil.

A segunda parte - o método get_serializer também está documentado - (um pouco mais abaixo na página de get_serializer_class em 'outros métodos'), portanto, deve ser seguro confiar nela (e a fonte é muito simples, o que, com sorte, significa menos chances de ataques não intencionais). efeitos secundários resultantes da modificação). Verifique a fonte no GenericAPIView (o ModelViewSet - e todas as outras classes de conjunto de visualizações que parece - herdadas do GenericAPIView, que define get_serializer.

Ao juntar os dois, você poderia fazer algo assim:

Em um arquivo serializadores (para mim 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

Então, no seu conjunto de visualizações, você pode fazer algo assim:

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)

E deve ser isso! Agora, o uso do MyViewSet deve instanciar o MyDynamicSerializer com os argumentos que você deseja - e assumindo que o serializador herda do DynamicFieldsModelSerializer, ele deve saber exatamente o que fazer.

Talvez valha a pena mencionar que pode fazer sentido especialmente se você quiser adaptar o serializador de outras maneiras ... por exemplo, para fazer coisas como pegar uma lista read_only_exceptions e usá-lo na lista de permissões em vez dos campos da lista negra (o que eu costumo fazer). Também acho útil definir os campos para uma tupla vazia, se não for aprovada, e apenas remover a verificação Nenhum ... e defino minhas definições de campos nos Serializadores herdados como ' all '. Isso significa que nenhum campo que não é passado ao instanciar o serializador sobrevive por acidente e também não preciso comparar a chamada do serializador com a definição de classe herdadora para saber o que foi incluído ... por exemplo, no init do DynamicFieldsModelSerializer:

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

Nota: se eu apenas quisesse duas ou três classes mapeadas para ações distintas e / ou não quisesse um comportamento serializador especialmente dinâmico, eu poderia usar uma das abordagens mencionadas por outras pessoas aqui, mas achei que vale a pena apresentar como alternativa , particularmente devido a seus outros usos.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.