Подтвердить что ты не робот

Динамически ограничивающий набор запросов в связанной области

Используя 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-интерфейсе API?

4b9b3361

Ответ 1

В итоге я сделал что-то похожее на то, что предложил Khamaileon. В основном, я модифицировал свой сериализатор, чтобы заглянуть в запрос, какой вид пахнет неправильно, но он выполняет свою работу... Вот как он выглядит (пример с примером покупки):

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 - это функция, которая берет пользователя и запрос и возвращает отфильтрованный запрос, который содержит только объекты, к которым у пользователя есть разрешение на связь. Кажется, что это работает как для проверки, так и для раскрывающихся полей API для просмотра.

Ответ 2

Вот как я это делаю:

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)

Ответ 3

Мне не нравился стиль переопределения метода init для каждого места, где мне нужно иметь доступ к пользовательским данным или экземпляру во время выполнения, чтобы ограничить набор запросов. Поэтому я выбрал это решение.

Вот код в строке.

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

Единственным недостатком этого является то, что вам нужно будет явно установить соответствующее поле сериализатора вместо использования автоматического обнаружения полей, предоставленного ModelSerializer. я бы ожидал, что что-то вроде этого будет в rest_framework по умолчанию.

Ответ 4

В django rest framework 3.0 метод get_fields был удален. Но аналогичным образом вы можете сделать это в функции init сериализатора:

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)

Я добавил проверку if, так как если вы используете PurchaseSerializer в качестве поля в другом сериализаторе при методах get, запрос не будет передан в контекст.

Ответ 5

Сначала убедитесь, что вы только разрешаете "self.request.user", когда у вас есть входящий HTTP POST/PUT (это предполагает, что свойство вашего сериализатора и модели буквально называется "пользователь" )

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) - о вашем фильтре выше (filter buyeriser = пользователь) Я бы рекомендовал использовать настраиваемый глобальный фильтр (чтобы убедиться, что он фильтруется по всему миру). Я делаю что-то для программного обеспечения как своего собственного приложения-службы, и это помогает гарантировать, что каждый HTTP-запрос будет отфильтрован (в том числе http 404, когда кто-то попытается найти "объект", у которого у них нет доступа, чтобы видеть в первую очередь )

Я недавно исправил это в главной ветки, поэтому оба списка и уникальные представления будут фильтровать этот

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

3) - о рендерерах api - есть ли у ваших клиентов это прямое использование? если нет, я бы сказал, избегай этого. Если вам это нужно, возможно добавить пользовательский serlializer, который поможет ограничить ввод на интерфейсе

Ответ 6

По запросу @gabn88, как вы уже знаете, с DRF 3.0 и выше, нет простого решения. Даже если вам удастся найти решение, оно не будет красивым и, скорее всего, не сработает на последующих версиях DRF, поскольку оно переопределит кучу источника DRF, который к тому времени будет изменен.

Я забыл точную реализацию, которую я использовал, но идея состоит в том, чтобы создать 2 поля в сериализаторе, одно ваше нормальное поле сериализатора (скажем, PrimaryKeyRelatedField и т.д.), а другое поле - поле метода сериализатора, результаты которого будут в некоторых случаях (например, на основе запроса, пользователя запроса или любого другого). Это будет сделано в конструкторе сериализаторов (т.е.: init)

В поле метода serializer будет возвращен пользовательский запрос. Вы побьете и/или замените эти результаты полей так, чтобы результаты вашего поля метода сериализатора были присвоены нормальному/серийному сериализатору поля (PrimaryKeyRelatedField и т.д.) Соответственно. Таким образом, вы всегда имеете дело с этим одним ключом (ваше поле по умолчанию), в то время как другой ключ остается прозрачным в вашем приложении.

Наряду с этой информацией все, что вам действительно нужно, это изменить это: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields

Ответ 7

Я написал собственный класс CustomQueryHyperlinkedRelatedField, чтобы обобщить это поведение:

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

Использование:

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

Ответ 8

Я сделал следующее:

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

Ответ 9

Ссылка на пример больше не доступна, но, читая другие комментарии, я предполагаю, что вы пытаетесь отфильтровать отношение пользователя к покупкам.

Если я прав, то могу сказать, что сейчас есть официальный способ сделать это. Протестировано с каркасом отдыха django 3.10.1.

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

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

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

Это работает также с API с возможностью просмотра.

Источники:

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