Quadro di riposo Django, utilizzare serializzatori diversi nello stesso ModelViewSet


197

Vorrei fornire due diversi serializzatori e poter comunque beneficiare di tutte le funzionalità di ModelViewSet:

  • Quando visualizzo un elenco di oggetti, vorrei che ogni oggetto avesse un URL che reindirizza ai suoi dettagli e che ogni altra relazione appare usando __unicode __il modello target;

esempio:

{
  "url": "http://127.0.0.1:8000/database/gruppi/2/",
  "nome": "universitari",
  "descrizione": "unitn!",
  "creatore": "emilio",
  "accesso": "CHI",
  "membri": [
    "emilio",
    "michele",
    "luisa",
    "ivan",
    "saverio"
  ]
}
  • Quando visualizzo i dettagli di un oggetto, vorrei usare l'impostazione predefinita HyperlinkedModelSerializer

esempio:

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

Sono riuscito a fare tutto questo come desidero nel modo seguente:

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.

Fondamentalmente rilevo quando l'utente richiede una vista elenco o una vista dettagliata e cambio serializer_classin base alle mie esigenze. Non sono davvero soddisfatto di questo codice, sembra un trucco sporco e, soprattutto, cosa succede se due utenti richiedono un elenco e un dettaglio nello stesso momento?

Esiste un modo migliore per raggiungere questo obiettivo ModelViewSetso devo ricadere utilizzando GenericAPIView?

EDIT:
Ecco come farlo utilizzando una base personalizzata 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.
    }

come lo hai implementato alla fine? Utilizzando il modo proposto dall'utente2734679 o utilizzando GenericAPIView?
Andilab

Come suggerito dall'utente2734679; Ho creato un ViewSet generico aggiungendo un dizionario per specificare il serializzatore per ogni azione e un serializzatore predefinito quando non specificato
BlackBear

Ho un problema simile ( stackoverflow.com/questions/24809737/… ) e per ora è finito con esso ( gist.github.com/andilab/a23a6370bd118bf5e858 ), ma non ne sono molto soddisfatto.
Andilab

1
Creato questo piccolo pacchetto per questo. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

1
Sostituisci metodo di recupero è OK.
Gzerone,

Risposte:


289

Sostituisci il tuo get_serializer_classmetodo. Questo metodo viene utilizzato nei mixin del modello per recuperare la classe Serializer corretta.

Si noti che esiste anche un get_serializermetodo che restituisce un'istanza del serializzatore corretto

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
Questo è fantastico, grazie! Ho ignorato get_serializer_class però
BlackBear

15
ATTENZIONE: django rest swagger non inserisce un parametro self.action, quindi questa funzione genererà un'eccezione. Potresti usare la risposta di Gonz o potresti usareif hasattr(self, 'action') and self.action == 'list'
Tom Leys,

Crea un piccolo pacchetto pypi per questo. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

Come possiamo ottenere l' pkoggetto richiesto, se l'azione è retrieve?
Pranjal Mittal,

My self.action è None. Qualcuno potrebbe dirmi perché?
Kakaji,

87

Potresti trovare utile questo mixin, sovrascrive il metodo get_serializer_class e ti consente di dichiarare un dict che associa azione e classe serializzatore o fallback al solito comportamento.

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

Creato questo piccolo pacchetto per questo. github.com/Darwesh27/drf-custom-viewsets
Adil Malik

15

Questa risposta è la stessa della risposta accettata, ma preferisco farlo in questo modo.

Viste generiche

get_serializer_class(self):

Restituisce la classe da utilizzare per il serializzatore. L'impostazione predefinita è la restituzione serializer_classdell'attributo.

Può essere sostituito per fornire un comportamento dinamico, come l'uso di serializzatori diversi per le operazioni di lettura e scrittura o la fornitura di serializzatori diversi ai diversi tipi di utenti. l'attributo 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)

Non posso usarlo perché mi dice che la mia vista non ha attributo "azione". Sembra ProductIndex (generics.ListCreateAPIView). Significa che devi assolutamente passare i viewet come argomento o c'è un modo per farlo usando le viste API generics?
Seb

1
una risposta tardiva al commento di @Seb - forse qualcuno può trarne profitto :) L'esempio usa ViewSets, non Views :)
fanny

Quindi, in combinazione con questo post stackoverflow.com/questions/32589087/... , ViewSets sembrano essere la strada da percorrere per avere più controllo sui diversi punti di vista e generare URL automaticamente per avere un'API consistente? Inizialmente pensava che generics.ListeCreateAPIView fosse il più efficiente, ma troppo semplice, giusto?
Seb

11

Per quanto riguarda la fornitura di serializzatori diversi, perché nessuno sceglie l'approccio che controlla il metodo HTTP? È più chiaro IMO e non richiede controlli extra.

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

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


12
Per il caso in questione, che riguarda l'uso di un serializzatore diverso per liste retrieveazioni, hai il problema che entrambi utilizzano il GETmetodo. Questo è il motivo per cui ViewSets di django rest framework utilizza il concetto di azioni , che è simile, ma leggermente diverso dai corrispondenti metodi http.
Håken Lid,

8

Sulla base delle risposte @gonz e @ user2734679, ho creato questo piccolo pacchetto Python che offre questa funzionalità in una classe figlio di ModelViewset. Ecco come funziona.

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
È meglio usare il mixin che è molto generico.
iamsk,

1

Anche se la pre-definizione di più serializzatori in un modo o nell'altro sembra essere il modo più evidentemente documentato , FWIW esiste un approccio alternativo che si basa su un altro codice documentato e che consente di passare argomenti al serializzatore quando viene istanziato. Penso che sarebbe probabilmente più utile se fosse necessario generare la logica in base a vari fattori, come i livelli di amministrazione dell'utente, l'azione chiamata, forse anche gli attributi dell'istanza.

Il primo pezzo del puzzle è la documentazione sulla modifica dinamica di un serializzatore nel punto di istanza . Tale documentazione non spiega come chiamare questo codice da un viewset o come modificare lo stato di sola lettura dei campi dopo che sono stati avviati, ma non è molto difficile.

Il secondo pezzo - è anche documentato il metodo get_serializer - (appena un po 'più in basso nella pagina da get_serializer_class sotto' altri metodi '), quindi dovrebbe essere sicuro fare affidamento su (e la fonte è molto semplice, il che si spera significhi meno possibilità di non intenzionali effetti collaterali derivanti dalla modifica). Controlla l'origine in GenericAPIView (ModelViewSet - e tutte le altre classi di set di viste integrate sembrano - ereditare da GenericAPIView che definisce get_serializer.

Mettendo insieme i due potresti fare qualcosa del genere:

In un file serializzatori (per me 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

Quindi nel tuo viewset potresti fare qualcosa del genere:

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 questo dovrebbe essere tutto! L'uso di MyViewSet dovrebbe ora creare un'istanza di MyDynamicSerializer con gli argomenti che desideri e, supponendo che il tuo serializzatore erediti da DynamicFieldsModelSerializer, dovrebbe sapere esattamente cosa fare.

Forse vale la pena ricordare che può avere un senso speciale se si desidera adattare il serializzatore in altri modi ... ad es. Per fare cose come prendere un elenco read_only_exceptions e usarlo per inserire nella whitelist piuttosto che nei campi della blacklist (cosa che tendo a fare). Trovo anche utile impostare i campi su una tupla vuota se non viene passato e quindi rimuovo il segno di controllo Nessuno, e imposto le definizioni dei miei campi sui miei serializzatori ereditari su " all ". Ciò significa che non campi che non sono passati quando si crea un'istanza della serializzatore sopravvivere per caso e ho anche non devono confrontare l'invocazione serializzatore con la definizione della classe che eredita serializer di sapere ciò che è stato incluso ... ad esempio, all'interno del init del DynamicFieldsModelSerializer:

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

NB Se volevo solo due o tre classi che si associassero ad azioni distinte e / o non volessi un comportamento serializzatore appositamente dinamico, potrei usare uno degli approcci citati da altri qui, ma ho pensato che valesse la pena presentarlo come alternativa , in particolare visti gli altri usi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.