45

使用 Django REST Framework,我想限制哪些值可以在创建中的相关字段中使用。

例如考虑这个示例(基于http://django-rest-framework.org/api-guide/filtering.html上的过滤示例,但更改为 ListCreateAPIView):

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

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

在此示例中,如何确保在创建时购买者可能仅等于 self.request.user,并且这是可浏览 API 渲染器中表单下拉列表中填充的唯一值?

4

10 回答 10

41

I ended up doing something similar to what Khamaileon suggested here. Basically I modified my serializer to peek into the request, which kind of smells wrong, but it gets the job done... Here's how it looks (examplified with the purchase-example):

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 is a function which takes a user and a query, and returns a filtered query which only contains objects that the user has permission to link to. This seems to work both for validation and for the browsable API dropdown fields.

于 2013-03-12T18:03:04.150 回答
16

Here's how I do it:

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)
于 2013-12-19T10:54:40.260 回答
12

The example link does not seem to be available anymore, but by reading other comments, I assume that you are trying to filter the user relationship to purchases.

If i am correct, then i can say that there is now an official way to do this. Tested with 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')

This works as well with the browsable API.

Sources:

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

于 2019-07-24T13:28:12.303 回答
8

I disliked the style of having to override the init method for every place where I need to have access to user data or the instance at runtime to limit the queryset. So I opted for this solution.

Here is the 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

The only drawback with this is that you would need to explicitly set the related serializer field instead of using the automatic field discovery provided by ModelSerializer. i would however expect something like this to be in rest_framework by default.

于 2015-11-03T02:27:15.027 回答
5

In django rest framework 3.0 the get_fields method was removed. But in a similar way you can do this in the init function of the serializer:

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)

I added the if check since if you use PurchaseSerializer as field in another serializer on get methods, the request will not be passed to the context.

于 2015-10-16T13:18:58.260 回答
3

首先确保当你有一个传入的 http POST/PUT 时你只允许“self.request.user”(这假设你的序列化器和模型上的属性字面上命名为“user”)

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

通过将上述内容添加到您的模型序列化程序中,您可以确保只有 request.user 被插入到您的数据库中。

2)-关于上面的过滤器(过滤器购买者=用户)我实际上建议使用自定义全局过滤器(以确保全局过滤)。我为自己的软件即服务应用程序做了一些事情,它有助于确保过滤掉每个 http 请求(包括当有人试图查找他们首先无权查看的“对象”时的 http 404 )

我最近在 master 分支中对此进行了修补,因此列表和单一视图都会过滤它

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

3) - about the api renderer - are you having your customers use this directly? if not I would say avoid it. If you need this it might be possible to add a custom serlializer that would help to limit the input on the front-end

于 2013-03-11T20:42:51.910 回答
1

Upon request @ gabn88, as you may know by now, with DRF 3.0 and above, there is no easy solution. Even IF you do manage to figure out a solution, it won't be pretty and will most likely fail on subsequent versions of DRF as it will override a bunch of DRF source which will have changed by then.

I forget the exact implementation I used, but the idea is to create 2 fields on the serializer, one your normal serializer field (lets say PrimaryKeyRelatedField etc...), and another field a serializer method field, which the results will be swapped under certain cases (such as based on the request, the request user, or whatever). This would be done on the serializers constructor (ie: init)

Your serializer method field will return a custom query that you want. You will pop and/or swap these fields results, so that the results of your serializer method field will be assigned to the normal/default serializer field (PrimaryKeyRelatedField etc...) accordingly. That way you always deal with that one key (your default field) while the other key remains transparent within your application.

Along with this info, all you really need is to modify this: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields

于 2015-09-14T19:41:40.757 回答
0

I wrote a custom CustomQueryHyperlinkedRelatedField class to generalize this behavior:

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)
    ...
于 2015-05-24T00:39:00.027 回答
0

I did the following:

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
于 2015-06-25T15:37:37.303 回答
0

I looked for a solution where I can set the queryset upon creation of the field and don't have to add a separate field class. This is what I came up with:

class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Purchase
        fields = ["purchaser"]

    def get_purchaser_queryset(self):
        user = self.context["request"].user
        return Purchase.objects.filter(purchaser=user)

    def get_extra_kwargs(self):
        kwargs = super().get_extra_kwargs()
        kwargs["purchaser"] = {"queryset": self.get_purchaser_queryset()}
        return kwargs

The main issue for tracking suggestions regarding this seems to be drf#1985.

于 2021-09-29T12:30:44.603 回答