wake-up-neo.com

Abfragesatz des zugehörigen Feldes dynamisch einschränken

Mit Django REST Framework möchte ich einschränken, welche Werte in einem verwandten Feld in einer Erstellung verwendet werden können.

Betrachten Sie zum Beispiel dieses Beispiel (basierend auf dem Filterungsbeispiel auf http://Django-rest-framework.org/api-guide/filtering.html , aber in ListCreateAPIView geändert):

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

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

Wie kann ich in diesem Beispiel sicherstellen, dass der Einkäufer bei der Erstellung möglicherweise nur self.request.user entspricht, und dass dies der einzige Wert ist, der in der Dropdown-Liste des Formulars im Browser-API-Renderer angezeigt wird?

35
Allanrbo

Am Ende tat ich etwas Ähnliches, was Khamaileon hier vorgeschlagen hatte . Grundsätzlich habe ich meinen Serializer so modifiziert, dass er in die Anfrage hineinschaut, welche Art von Gerüchen falsch ist, aber die Arbeit wird erledigt ... So sieht es aus (am Kaufbeispiel erläutert):

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

allowed_objects ist eine Funktion, die einen Benutzer und eine Abfrage akzeptiert und eine gefilterte Abfrage zurückgibt, die nur Objekte enthält, zu denen der Benutzer eine Verknüpfung hat. Dies scheint sowohl für die Überprüfung als auch für die durchsuchbaren API-Dropdown-Felder zu funktionieren.

35
Allanrbo

So mache ich es:

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)
13
dustinfarris

Ich mochte den Stil nicht, die init-Methode für jeden Ort überschreiben zu müssen, an dem ich Zugriff auf Benutzerdaten oder die Instanz zur Laufzeit haben muss, um das Queryset einzuschränken. Also entschied ich mich für diese Lösung .

Hier ist der Code inline.

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

Der einzige Nachteil dabei ist, dass Sie das zugehörige Serialisierungsfeld explizit festlegen müssen, anstatt die automatische Felderkennung von ModelSerializer zu verwenden. Ich würde jedoch erwarten, dass so etwas standardmäßig in rest_framework ist.

7
user2059857

In Django rest framework 3.0 wurde die get_fields-Methode entfernt. Auf ähnliche Weise können Sie dies in der init-Funktion des Serializers tun:

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)

Ich habe die if-Prüfung hinzugefügt, da die Anforderung nicht an den Kontext übergeben wird, wenn Sie PurchaseSerializer als Feld in einem anderen Serializer für get-Methoden verwenden.

4
Vlad

Stellen Sie zunächst sicher, dass Sie "self.request.user" nur zulassen, wenn Sie über einen eingehenden HTTP-POST/PUT verfügen (dies setzt voraus, dass die Eigenschaft Ihres Serializers und das Modell buchstäblich "user" genannt werden).

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

Indem Sie das obige zu Ihrem Modell-Serialisierer hinzufügen, stellen Sie sicher, dass NUR der request.user in Ihre Datenbank eingefügt wird.

2) -Über Ihren Filter oben (Filter Einkäufer = Benutzer) Ich würde tatsächlich empfehlen, einen benutzerdefinierten globalen Filter zu verwenden (um sicherzustellen, dass dieser global gefiltert wird). Ich mache etwas für eine Software als eigene Service-App und es hilft sicherzustellen, dass jede http-Anfrage nach unten gefiltert wird (einschließlich eines http 404, wenn jemand versucht, ein "Objekt" zu suchen, auf das er überhaupt keinen Zugriff hat )

Ich habe dies kürzlich im Master-Zweig gepatcht, sodass sowohl Listen- als auch Einzelansichten dies filtern

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

3) - über den API-Renderer - Lassen Sie Ihre Kunden dies direkt verwenden? wenn nicht, würde ich es vermeiden. Wenn Sie dies benötigen, können Sie möglicherweise einen benutzerdefinierten Serializer hinzufügen, um die Eingabe am Front-End zu begrenzen

3
Toran Billups

Der Beispiellink scheint nicht mehr verfügbar zu sein, aber wenn Sie andere Kommentare lesen, gehe ich davon aus, dass Sie versuchen, die Benutzerbeziehung zu Käufen zu filtern.

Wenn ich richtig liege, kann ich sagen, dass es jetzt einen offiziellen Weg gibt, dies zu tun. Getestet mit Django rest framework 3.10.1.

class UserPKField(serializers.PrimaryKeyRelatedField):
    def get_queryset(self):
        user = self.context['request'].user
        queryset = User.objects.filter(...)
        return queryset

class PurchaseSeriaizer(serializers.ModelSerializer):
    users = UserPKField(many=True)

    class Meta:
        model = Purchase
        fields = ('id', 'users')

Dies funktioniert auch mit der durchsuchbaren API.

Quellen:

https://github.com/encode/Django-rest-framework/issues/1985#issuecomment-328366412

https://medium.com/Django-rest-framework/limit-related-data-choices-with-Django-rest-framework-c54e96f5815e

1
Agey

Auf Anfrage @ gabn88, wie Sie vielleicht bereits wissen, mit DRF 3.0 und höher gibt es keine einfache Lösung. Selbst wenn Sie eine Lösung finden, ist sie nicht hübsch und wird wahrscheinlich nicht funktionieren spätere Versionen von DRF, da sie eine Reihe von DRF-Quellen überschreiben, die sich bis dahin geändert haben.

Ich habe die genaue Implementierung vergessen, die ich verwendet habe, aber die Idee ist, zwei Felder auf dem Serialisierer zu erstellen, eines Ihr normales Serialisierfeld (sagen wir PrimaryKeyRelatedField usw.) und ein anderes Feld ein Serialisiermethodenfeld, unter dem die Ergebnisse ausgelagert werden bestimmte Fälle (z. B. basierend auf der Anfrage, dem Request-Benutzer oder was auch immer). Dies würde auf dem Serializer-Konstruktor erfolgen (dh: init).

Ihr Feld für die Serialisierermethode gibt eine benutzerdefinierte Abfrage zurück, die Sie möchten . Sie werden die Ergebnisse dieser Felder anzeigen und/oder austauschen, sodass die Ergebnisse Ihres Serialisierermethodenfelds dem normalen/Standard-Serialisierfeld (PrimaryKeyRelatedField usw.) zugewiesen werden. ..) entsprechend. Auf diese Weise bearbeiten Sie immer diesen einen Schlüssel (Ihr Standardfeld), während der andere Schlüssel in Ihrer Anwendung transparent bleibt.

Neben diesen Informationen müssen Sie lediglich Folgendes ändern: http://www.Django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields

1
Andrew P

Ich habe eine benutzerdefinierte CustomQueryHyperlinkedRelatedField-Klasse geschrieben, um dieses Verhalten zu verallgemeinern:

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

Verwendungszweck:

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
kauai diver

Ich habe folgendes gemacht:

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
0
Jon