web-dev-qa-db-fra.com

Limiter dynamiquement le jeu de requêtes du champ associé

En utilisant Django REST Framework, je souhaite limiter les valeurs pouvant être utilisées dans un champ associé dans une création.

Par exemple, considérons cet exemple (basé sur l'exemple de filtrage de http://Django-rest-framework.org/api-guide/filtering.html , mais remplacé par ListCreateAPIView):

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

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

Dans cet exemple, comment puis-je m'assurer que, lors de la création, l'acheteur ne peut être égal qu'à self.request.user et qu'il s'agit de la seule valeur renseignée dans la liste déroulante du formulaire dans le rendu d'API pouvant être parcouru?

35
Allanrbo

J'ai fini par faire quelque chose de similaire à ce que Khamaileon a suggéré ici . En gros, j'ai modifié mon sérialiseur pour jeter un coup d'œil dans la demande, ce qui ne sent pas bon, mais le travail est fait. Voici à quoi cela ressemble (exemple avec l'exemple d'achat):

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 est une fonction qui prend un utilisateur et une requête et renvoie une requête filtrée qui ne contient que des objets pour lesquels l'utilisateur est autorisé à créer un lien. Cela semble fonctionner à la fois pour la validation et pour les champs de liste déroulante des API.

35
Allanrbo

Voici comment je le fais:

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

Je n'aimais pas le style de devoir redéfinir la méthode init pour chaque endroit où je dois avoir accès aux données utilisateur ou à l'instance au moment de l'exécution pour limiter le jeu de requêtes. J'ai donc opté pour cette solution .

Voici le code en ligne.

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

Le seul inconvénient est que vous devez définir explicitement le champ du sérialiseur associé au lieu d'utiliser la découverte de champ automatique fournie par ModelSerializer. Je m'attendrais toutefois à ce que quelque chose comme ce soit dans rest_framework par défaut.

7
user2059857

Dans Django Rest Framework 3.0, la méthode get_fields a été supprimée. Mais de la même manière, vous pouvez le faire dans la fonction init du sérialiseur:

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)

J'ai ajouté le if check puisque si vous utilisez PurchaseSerializer comme champ dans un autre sérialiseur avec les méthodes get, la demande ne sera pas transmise au contexte.

4
Vlad

Tout d'abord, assurez-vous que vous n'autorisez "self.request.user" que lorsque vous avez un http POST/PUT entrant (cela suppose que la propriété de votre sérialiseur et le modèle s'appelle littéralement "utilisateur")

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

En ajoutant ce qui précède à votre sérialiseur de modèle, vous vous assurez que SEULEMENT le request.user est inséré dans votre base de données.

2) -au sujet de votre filtre ci-dessus (acheteur de filtre = utilisateur), je recommanderais en fait d'utiliser un filtre global personnalisé (pour s'assurer qu'il est filtré globalement). Je fais quelque chose pour un logiciel en tant qu'application de service et cela permet de s'assurer que chaque demande http est filtrée (y compris un http 404 lorsque quelqu'un essaie de rechercher un "objet" auquel il n'a pas accès). )

J'ai récemment corrigé cela dans la branche principale afin que les vues liste et singulière filtrent cette

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

3) - à propos du moteur de rendu api - vos clients l’utilisent-ils directement? sinon je dirais de l'éviter. Si vous en avez besoin, vous pouvez éventuellement ajouter un sérialiseur personnalisé qui aiderait à limiter les entrées sur le front-end.

3
Toran Billups

Sur demande @ gabn88, comme vous le savez peut-être déjà, avec DRF 3.0 et versions ultérieures, il n’ya pas de solution facile . Même si vous parvenez à trouver une solution, celle-ci ne sera pas jolie et échouera probablement avec les versions ultérieures de DRF car elles remplaceront un groupe de sources DRF qui auront changé à ce moment-là.

J'oublie l'implémentation exacte que j'ai utilisée, mais l'idée est de créer 2 champs sur le sérialiseur, l'un votre champ de sérialiseur normal (disons, PrimaryKeyRelatedField etc ...), et un autre champ, un champ de méthode de sérialiseur, dans lequel les résultats seront échangés. certains cas (comme basé sur la demande, l'utilisateur de la demande, ou autre). Cela serait fait sur le constructeur de sérialiseurs (ie: init)

Votre champ de méthode de sérialiseur renverra la requête personnalisée que vous voulez . Vous allez extraire et/ou échanger ces résultats afin que les résultats de votre champ de méthode de sérialiseur soient affectés au champ de sérialiseur normal/par défaut (PrimaryKeyRelatedField, etc.). ..) en conséquence. Ainsi, vous utiliserez toujours cette clé (votre champ par défaut), tandis que l’autre clé reste transparente au sein de votre application.

En plus de cette information, tout ce dont vous avez besoin est de modifier ceci: http://www.Django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields

1
Andrew P

J'ai écrit une classe personnalisée CustomQueryHyperlinkedRelatedField pour généraliser ce comportement:

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

J'ai fait ce qui suit:

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