【问题标题】:Dynamically limiting queryset of related field动态限制相关字段的查询集
【发布时间】:2013-02-26 01:00:31
【问题描述】:

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

例如考虑这个例子(基于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 渲染器中表单的下拉列表中填充的唯一值?

【问题讨论】:

    标签: python django django-rest-framework


    【解决方案1】:

    我最终做了类似于Khamaileon suggested here 的事情。基本上我修改了我的序列化程序以窥探请求,哪种味道不对,但它完成了工作......这是它的外观(以购买示例为例):

    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 是一个函数,它接受一个用户和一个查询,并返回一个过滤后的查询,该查询只包含用户有权链接到的对象。这似乎适用于验证和可浏览的 API 下拉字段。

    【讨论】:

    • 在我的例子中,我使用了另一个 ModelSerializer 作为字段,所以我需要做一个更大的综述......
    • 使用 DRF 3.0,这不再起作用,因为它们的查询集越来越深地嵌套在 field.child_relation 中。
    • 有什么方法可以在 DRF 3.1 上完成同样的任务吗?
    • 是的,可以这样做,我将在下面添加解决方案
    • 我将在下面发布您可以做到的方式。
    【解决方案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】:

      示例链接似乎不再可用,但通过阅读其他 cmets,我假设您正在尝试过滤用户与购买的关系。

      如果我是正确的,那么我可以说现在有一种官方方法可以做到这一点。使用 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')
      
      

      这也适用于可浏览的 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

      【讨论】:

        【解决方案4】:

        我不喜欢在运行时需要访问用户数据或实例以限制查询集的每个地方都必须重写 init 方法的风格。所以我选择了this solution。

        这是内联代码。

        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 中。

        【讨论】:

          【解决方案5】:

          在 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 检查,因为如果您在 get 方法的另一个序列化程序中使用 PurchaseSerializer 作为字段,请求将不会传递到上下文。

          【讨论】:

            【解决方案6】:

            首先要确保当你有一个传入的 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) - 关于 api 渲染器 - 您是否让您的客户直接使用它?如果不是,我会说避免它。如果您需要这个,可以添加一个自定义序列化程序,这将有助于限制前端的输入

            【讨论】:

            • 感谢您的建议!我实际上希望我可以让我的客户直接使用可浏览的 API,这样他们就可以进行探索和实验。一个问题:您将在哪里/如何实现您提到的自定义全局过滤器?
            • 当然,首先 pip install django-filter。下一个子类 DjangoFilterBackend 并覆盖 filter_queryset 方法(链接你的 customer.filter)。最后添加设置以告诉 DRF 您希望在每个请求中应用此过滤器django-rest-framework.org/api-guide/…
            【解决方案7】:

            根据@gabn88 的请求,您现在可能知道,对于 DRF 3.0 及更高版本,没有简单的解决方案。 即使您确实设法找到了解决方案,它也不会很漂亮,并且很可能在 DRF 的后续版本中失败,因为它会覆盖到那时已经更改的一堆 DRF 源。

            我忘记了我使用的确切实现,但想法是在序列化程序上创建 2 个字段,一个是您的普通序列化程序字段(比如说 PrimaryKeyRelatedField 等...),另一个字段是序列化程序方法字段,结果将在某些情况下(例如基于请求、请求用户或其他)进行交换。这将在序列化程序构造函数上完成(即:init)

            您的序列化程序方法字段将返回您想要的自定义查询。 您将弹出和/或交换这些字段结果,以便您的序列化程序方法字段的结果将相应地分配给正常/默认序列化程序字段(PrimaryKeyRelatedField 等...)。这样一来,您始终处理那个键(您的默认字段),而另一个键在您的应用程序中保持透明。

            除此信息外,您真正需要的是修改此信息:http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields

            【讨论】:

              【解决方案8】:

              我编写了一个自定义的 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)
                  ...
              

              【讨论】:

                【解决方案9】:

                我做了以下事情:

                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
                

                【讨论】:

                  【解决方案10】:

                  我寻找了一种解决方案,我可以在创建字段时设置查询集,而不必添加单独的字段类。这是我想出的:

                  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
                  

                  跟踪建议的主要问题似乎是drf#1985。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2021-09-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-12-05
                    • 2012-01-01
                    • 2020-07-31
                    • 1970-01-01
                    相关资源
                    最近更新 更多