2013-03-10 14 views
30

Utilizzando Django REST Framework, voglio limitare quali valori possono essere utilizzati in un campo correlato in una creazione.Queryset limitante dinamicamente del campo correlato

Ad esempio considerare questo esempio (secondo l'esempio filtraggio http://django-rest-framework.org/api-guide/filtering.html, ma modificati per ListCreateAPIView):

class PurchaseList(generics.ListCreateAPIView) 
    model = Purchase 
    serializer_class = PurchaseSerializer 

    def get_queryset(self): 
     user = self.request.user 
     return Purchase.objects.filter(purchaser=user) 

In questo esempio, come posso garantire che sulla creazione l'acquirente può essere solo uguale a sé .request.user e che questo è l'unico valore inserito nel menu a discesa nel modulo nel renderer API navigable?

risposta

31

Ho finito per fare qualcosa di simile a quello Khamaileon suggested here. Fondamentalmente ho modificato il mio serializzatore di sbirciare nella richiesta, che tipo di odori sbagliato, ma ottiene il lavoro fatto ... Ecco come appare (examplified con l'acquisto-esempio):

class PurchaseSerializer(serializers.HyperlinkedModelSerializer): 
    def get_fields(self, *args, **kwargs): 
     fields = super(PurchaseSerializer, self).get_fields(*args, **kwargs) 
     fields['purchaser'].queryset = permitted_objects(self.context['view'].request.user, fields['purchaser'].queryset) 
     return fields 

    class Meta: 
     model = Purchase 

permitted_objects è una funzione che accetta un utente e una query e restituisce una query filtrata che contiene solo oggetti a cui l'utente ha il permesso di collegarsi. Questo sembra funzionare sia per la validazione che per i campi dropdown dell'API navigabili.

+0

+1 grazie per questo – Fiver

+0

Nel mio caso I usato un altro ModelSerializer come campo, quindi avevo bisogno di fare un rastrellamento più grande ... – alanjds

+0

+1 grazie per questo –

3

primo a essere sicuri di consentire solo "self.request.user" quando si dispone di un ingresso http POST/PUT (Premesso che la proprietà sulla vostra serializzatore e il modello è denominato "utente", letteralmente)

def validate_user(self, attrs, source): 
    posted_user = attrs.get(source, None) 
    if posted_user: 
     raise serializers.ValidationError("invalid post data") 
    else: 
     user = self.context['request']._request.user 
     if not user: 
      raise serializers.ValidationError("invalid post data") 
     attrs[source] = user 
    return attrs 

Aggiungendo quanto sopra al serializzatore del modello, si assicura che SOLO il request.user sia inserito nel proprio database.

2) -caso sopra il filtro (acquirente di filtri = utente) In realtà mi consiglia di utilizzare un filtro globale personalizzato (per garantire che venga filtrato globalmente). Faccio qualcosa per un software come app di servizio personale e mi aiuta a garantire che ogni richiesta http venga filtrata (incluso un http 404 quando qualcuno cerca di cercare un "oggetto" a cui non hanno accesso per vedere in primo luogo)

recentemente ho patchato questo nel ramo master in modo sia la lista e vista singolari sarà filtrare questo

https://github.com/tomchristie/django-rest-framework/commit/1a8f07def8094a1e34a656d83fc7bdba0efff184

3) - sulla renderer api - stai avendo i clienti utilizzano questo direttamente? se no, direi di evitarlo. Se necessario, potrebbe essere possibile aggiungere un serializzatore personalizzato che potrebbe aiutare a limitare l'input sul front-end

+0

Grazie per i vostri suggerimenti! Speravo davvero che potessi permettere ai miei clienti di utilizzare direttamente l'API navigabile, in modo che potessero esplorare e sperimentare. Una domanda: dove/come implementeresti il ​​filtro globale personalizzato che hai menzionato? – Allanrbo

+1

Certo, primo pip installare il filtro Django. Next sottoclasse DjangoFilterBackend e sovrascrive il metodo filter_queryset (per incatenare il tuo customer.filter). Infine aggiungi le impostazioni per dire a DRF che vuoi applicare questo filtro ad ogni richiesta http://django-rest-framework.org/api-guide/filtering.html#generic-filtering –

11

Ecco come lo faccio:

class PurchaseList(viewsets.ModelViewSet): 
    ... 
    def get_serializer(self, *args, **kwargs): 
     serializer_class = self.get_serializer_class() 
     context = self.get_serializer_context() 
     return serializer_class(*args, request_user=self.request.user, context=context, **kwargs) 

class PurchaseSerializer(serializers.ModelSerializer): 
    ... 
    def __init__(self, *args, request_user=None, **kwargs): 
     super(PurchaseSerializer, self).__init__(*args, **kwargs) 
     self.fields['user'].queryset = User._default_manager.filter(pk=request_user.pk) 
+2

Grazie! Questo funziona per me in DRF 3.1.2 –

0

ho scritto una classe CustomQueryHyperlinkedRelatedField personalizzato generalizzare questo comportamento:

class CustomQueryHyperlinkedRelatedField(serializers.HyperlinkedRelatedField): 
    def __init__(self, view_name=None, **kwargs): 
     self.custom_query = kwargs.pop('custom_query', None) 
     super(CustomQueryHyperlinkedRelatedField, self).__init__(view_name, **kwargs) 

    def get_queryset(self): 
     if self.custom_query and callable(self.custom_query): 
      qry = self.custom_query()(self) 
     else: 
      qry = super(CustomQueryHyperlinkedRelatedField, self).get_queryset() 

     return qry 

    @property 
    def choices(self): 
     qry = self.get_queryset() 
     return OrderedDict([ 
      (
       six.text_type(self.to_representation(item)), 
       six.text_type(item) 
      ) 
      for item in qry 
     ]) 

Usage:

class MySerializer(serializers.HyperlinkedModelSerializer): 
    .... 
    somefield = CustomQueryHyperlinkedRelatedField(view_name='someview-detail', 
         queryset=SomeModel.objects.none(), 
         custom_query=lambda: MySerializer.some_custom_query) 

    @staticmethod 
    def some_custom_query(field): 
     return SomeModel.objects.filter(somefield=field.context['request'].user.email) 
    ... 
0

ho fatto quanto segue:

class MyModelSerializer(serializers.ModelSerializer): 
    myForeignKeyFieldName = MyForeignModel.objects.all() 

    def get_fields(self, *args, **kwargs): 
     fields = super(MyModelSerializer, self).get_fields() 
     qs = MyModel.objects.filter(room=self.instance.id) 
     fields['myForeignKeyFieldName'].queryset = qs 
     return fields 
1

Su richiesta @ gabn88, come forse saprai, con DRF 3.0 e versioni successive non esiste una soluzione facile. Anche se riuscite a capire una soluzione, non sarà carina e molto probabilmente fallirà nelle versioni successive di DRF poiché sovrascriverà una serie di sorgenti DRF che saranno cambiate a quel punto.

Ho dimenticato l'implementazione esatta che ho usato, ma l'idea è di creare 2 campi sul serializzatore, un campo di serializzazione normale (diciamo PrimaryKeyRelatedField ecc.), E un altro campo un campo di metodo serializzatore, che i risultati sarà scambiato in determinati casi (come in base alla richiesta, all'utente della richiesta, o qualsiasi altra cosa). Questo dovrebbe essere fatto sul costruttore serializzatori (es .: init)

Il campo del metodo serializzatore restituirà una query personalizzata che si desidera. Si apriranno e/o scambieranno i risultati di questi campi, in modo che i risultati del campo del metodo serializzatore verranno assegnati al campo serializzatore normale/predefinito (PrimaryKeyRelatedField ecc ...) di conseguenza. In questo modo ti occupi sempre di quell'unica chiave (il tuo campo predefinito) mentre l'altra chiave rimane trasparente all'interno della tua applicazione.

Insieme a queste informazioni, tutto ciò che ha realmente bisogno è di modificare il presente: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields

4

In Django 3.0 metodo get_fields è stato rimosso. Ma in un modo simile si può fare questo nella funzione init del serializzatore:

class PurchaseSerializer(serializers.HyperlinkedModelSerializer): 
    class Meta: 
     model = Purchase 

    def __init__(self, *args, **kwargs): 
     super(PurchaseSerializer, self).__init__(*args, **kwargs) 
     if 'request' in self.context: 
      self.fields['purchaser'].queryset = permitted_objects(self.context['view'].request.user, fields['purchaser'].queryset) 

ho aggiunto il caso di controllo dal momento che se si utilizza PurchaseSerializer come campo di un'altra serializzatore su metodi get, non sarà superato la richiesta di il contesto.

5

Non mi piace lo stile di dover sovrascrivere il metodo init per ogni luogo in cui è necessario avere accesso ai dati utente o all'istanza in fase di esecuzione per limitare il set di query. Quindi ho optato per this solution.

Ecco il codice in linea.

from rest_framework import serializers 


class LimitQuerySetSerializerFieldMixin: 
    """ 
    Serializer mixin with a special `get_queryset()` method that lets you pass 
    a callable for the queryset kwarg. This enables you to limit the queryset 
    based on data or context available on the serializer at runtime. 
    """ 

    def get_queryset(self): 
     """ 
     Return the queryset for a related field. If the queryset is a callable, 
     it will be called with one argument which is the field instance, and 
     should return a queryset or model manager. 
     """ 
     # noinspection PyUnresolvedReferences 
     queryset = self.queryset 
     if hasattr(queryset, '__call__'): 
      queryset = queryset(self) 
     if isinstance(queryset, (QuerySet, Manager)): 
      # Ensure queryset is re-evaluated whenever used. 
      # Note that actually a `Manager` class may also be used as the 
      # queryset argument. This occurs on ModelSerializer fields, 
      # as it allows us to generate a more expressive 'repr' output 
      # for the field. 
      # Eg: 'MyRelationship(queryset=ExampleModel.objects.all())' 
      queryset = queryset.all() 
     return queryset 


class DynamicQuersetPrimaryKeyRelatedField(LimitQuerySetSerializerFieldMixin, serializers.PrimaryKeyRelatedField): 
    """Evaluates callable queryset at runtime.""" 
    pass 


class MyModelSerializer(serializers.ModelSerializer): 
    """ 
    MyModel serializer with a primary key related field to 'MyRelatedModel'. 
    """ 
    def get_my_limited_queryset(self): 
     root = self.root 
     if root.instance is None: 
      return MyRelatedModel.objects.none() 
     return root.instance.related_set.all() 

    my_related_model = DynamicQuersetPrimaryKeyRelatedField(queryset=get_my_limited_queryset) 

    class Meta: 
     model = MyModel 

L'unico svantaggio di questo è che si avrebbe bisogno di impostare in modo esplicito il relativo campo serializzatore invece di utilizzare la scoperta del giacimento automatico fornito da ModelSerializer. mi aspetterei comunque che qualcosa del genere si trovi in ​​rest_framework di default.

Problemi correlati