使用多个关键字对象引用的 Django Rest Framework 对象



我有一个模型,它对两个字段具有唯一的约束:

class Document(models.Model):
    filename = models.CharField(max_length=255)
    publication = models.CharField(max_length=8)
    class Meta:
        constraints = [
                models.UniqueConstraint(
                    fields=['filename', 'publication'], name='document_key')

根据DRF GenericAPIView get_object方法中的文档:

    """
    Returns the object the view is displaying.
    You may want to override this if you need to provide non-standard
    queryset lookups.  Eg if objects are referenced using multiple
    keyword arguments in the url conf.
    """

使用多个关键字参数引用正是我想要做的。我已经开始覆盖get_object方法

class DocumentViewset(viewsets.ModelViewSet):
    serializer_class = serializers.ActSerializer 
    lookup_fields = ('filename', 'publication')
    def get_object(self):
        """As per the example in the DRF docstring itself,
        objects are referenced using multiple keyword arguments
        in the URL conf. Therefore, we need to override. 
        """
        queryset = self.filter_queryset(self.get_queryset())
        lookup_url_kwargs = self.lookup_fields
        print(lookup_url_kwargs)
        print(self.kwargs)

这给了我:

('filename', 'publication')
{'pk': '17'}

你可以看到问题在于我的lookup_url_kwargs不会在self.kwargs中(在下一行中验证(。如果设置了"lookup_url_kwarg",那么self.kwargs就是这样。但是如果没有它,self.kwargs默认为"pk"。如何覆盖此行为,以便在 URL 中需要两个字段?谢谢。

(

有几种方法可以获得所需的行为,复杂程度越来越高:


第一种方式:

不要像往常一样使用 DRF 路由器并手动定义 URL 端点。另外,请参阅正确的映射,例如,有关带有 URL 关键字参数的详细 URL,filename publication

urlpatterns = [
    path(
        'documents/<filename:str>/<publication:str>/',
        DocumentViewset.as_view({'get': 'retrieve'}),
        name='document-detail',
    ),
    ...
]

现在,您将在方法retrieve self.kwargs中获得具有相应值的filenamepublication键。

您可以添加路径转换器,以便更好地控制每个网址关键字匹配项允许的模式。例如,如果按-而不是/分隔 URL(因为 / 通常意味着子资源(,则 URL 在逻辑上可能看起来更好。在这里,我们创建了两个转换器,用于抓取破折号(-(之前和之后的部分:

class BaseDashConverter:  
    def to_python(self, value):
        return value
    def to_url(self, value):
        return value
class BeforeDashConverter(BaseDashConverter):
    regex = '[^-]+'
class AfterDashConverter(BaseDashConverter):
    regex = '[^/]+'

现在,是时候注册这两个了:

register_converter(BeforeDashConverter, 'before-dash')
register_converter(AfterDashConverter, 'after-dash')

然后在urlpatterns中,您可以执行以下操作:

urlpatterns = [
    path(
        'documents/<filename:before-dash>-<publication:after-dash>/',
        DocumentViewset.as_view({'get': 'retrieve'}),
        name='document-detail',
    ),
    ...
]

您也可以直接将re_path与正则表达式一起使用,而不是创建转换器并使用path

urlpatterns = [
    re_path(
        'documents/(?P<filename>[^-]+)-(?P<publication>[^/]+)/',
        DocumentViewset.as_view({'get': 'retrieve'}),
        name='document-detail',
    ),
    ...
]

FWIW,您需要为所有方法-操作映射添加 URL 路径,就像我为 get - retrieve 所做的那样。


第二种方式:

假设lookup_fields是您的自定义属性(视图集实际上使用属性 lookup_field 引用的单个字段(,您可以将lookup_kwargs命名为 combined 之类的名称,并将 URL 中用 - 分隔的filenamepublication作为/documents/<filename>-<publication>/例如 /documents/somename-foobar/ 。DRF Router 的详细信息查找匹配除前缀后面的 ./ 之外的一个或多个字符,因此将匹配此字符。

如果这样做,则可以在get_object中添加自定义逻辑,例如:

class DocumentViewset(viewsets.ModelViewSet):
    serializer_class = serializers.ActSerializer 
    lookup_fields = ('filename', 'publication')
    lookup_url_kwarg = 'combined'
    def get_object(self):
        queryset = self.filter_queryset(self.get_queryset())
        lookup_url_kwarg = self.lookup_url_kwarg
        assert lookup_url_kwarg in self.kwargs, (
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )
        combined = self.kwargs[lookup_url_kwarg]
        filter_kwargs = dict(zip(self.lookup_fields, combined.partition('-')[::2]))
        obj = get_object_or_404(queryset, **filter_kwargs)
        self.check_object_permissions(self.request, obj)
        return obj

第三种方式:

覆盖 DRF 的Router并在 get_lookup_regex 方法中添加自定义正则表达式以匹配包括 filenamepublication 的 URL 路径。

下面是一个示例,与前面的方法一样,它将匹配/documents/<filename>-<publication>/ 形式的任何模式(例如 /documents/somename-foobar/ (,但现在 URL 关键字参数将有两个键:filenamepublication 。您可以想象的那样根据自己的喜好更改图案/格式。

首先,我们需要定义具有覆盖get_lookup_regex的自定义路由器:

from rest_framework.routers import DefaultRouter
class CustomRouter(DefaultRouter):
    def get_lookup_regex(self, viewset, lookup_prefix=''):
        lookup_fields = getattr(viewset, 'lookup_fields', ('filename', 'publication'))
        lookup_url_kwargs = getattr(viewset, 'lookup_url_kwargs', lookup_fields)
        return (
            rf'(?P<{lookup_prefix}{lookup_url_kwargs[0]}>[^-]+)-'
            rf'(?P<{lookup_prefix}{lookup_url_kwargs[1]}>[^/.]+)'
        )

因此,这将检查类中的lookup_fieldslookup_url_kwargs ViewSet以根据匹配的模式设置正则表达式关键字。

注册视图集将照常进行:

router = CustomRouter()
router.register('documents', DocumentViewSet)

利用上述路由器的DocumentViewset可以如下所示,覆盖get_querysetlookup_fields/lookup_url_kwargs集:

class DocumentViewset(viewsets.ModelViewSet):
    serializer_class = serializers.ActSerializer 
    lookup_fields = ('filename', 'publication')
    lookup_url_kwargs = ('filename', 'publication')
    def get_object(self):
        queryset = self.filter_queryset(self.get_queryset())
        lookup_url_kwargs = self.lookup_url_kwargs or self.lookup_fields
        assert all(
            lookup_kwarg in self.kwargs
            for lookup_kwarg in lookup_url_kwargs
        ), (
            'Expected view %s to be called with URL keyword arguments '
            'named "%s". Fix your URL conf, or set the `.lookup_fields` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, ','.join(lookup_url_kwargs))
        )
        field_values = (self.kwargs[lookup_kwarg] for lookup_kwarg in lookup_url_kwargs)
        filter_kwargs = dict(zip(self.lookup_fields, field_values))
        obj = get_object_or_404(queryset, **filter_kwargs)
        # May raise a permission denied
        self.check_object_permissions(self.request, obj)
        return obj

以上每一项都会让您获得所需的行为。选择最适合您的一种。

注意:-作为 seaparator 的用法 在示例中,您可以选择自己的分隔符。但是,如果要使用 /. 作为分隔符,则需要使用第一种或第三种方式,因为第二种方式使用 DefaultRouter.get_lookup_regex 模式的数学运算到下一个/.

最新更新