DRF: маршрутизатор для ViewSet с полем поиска_иностранного ключа

Используя Django REST Framework 3.12.4, я не могу правильно заставить URL-адреса для набора ViewSet работать, если этот набор ViewSet имеет поле поиска иностранного ключа

У меня есть следующее в models.py:

class Domain(models.Model):
    name = models.CharField(max_length=191, unique=True)


class Token(rest_framework.authtoken.models.Token):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    domain_policies = models.ManyToManyField(Domain, through='TokenDomainPolicy')


class TokenDomainPolicy(models.Model):
    token = models.ForeignKey(Token, on_delete=models.CASCADE)
    domain = models.ForeignKey(Domain, on_delete=models.CASCADE)

    class Meta:
        constraints = [models.UniqueConstraint(fields=['token', 'domain'], name='unique_entry')]

В views.py у меня есть:

class TokenDomainPolicyViewSet(viewsets.ModelViewSet):
    lookup_field = 'domain__name'
    permission_classes = (IsAuthenticated,)
    serializer_class = serializers.TokenDomainPolicySerializer

    def get_queryset(self):
        return models.TokenDomainPolicy.objects.filter(token_id=self.kwargs['id'], token__user=self.request.user)

Из TokenDomainPolicyViewSet.lookup_field видно, что я хотел бы иметь возможность запрашивать конечную точку -detail, используя поле Domain связанной точки name вместо ее первичного ключа. (name является уникальным для данного токена.)

Я думал, что это можно сделать с помощью lookup_field = 'domain__name'.

Однако это не совсем работает. Вот мой urls.py:

tokens_router = SimpleRouter()
tokens_router.register(r'', views.TokenViewSet, basename='token')

tokendomainpolicies_router = SimpleRouter()
tokendomainpolicies_router.register(r'', views.TokenDomainPolicyViewSet, basename='token_domain_policies')

auth_urls = [
    path('tokens/', include(tokens_router.urls)),  # for completeness only; problem persists if removed
    path('tokens/<id>/domain_policies/', include(tokendomainpolicies_router.urls)),
]

urlpatterns = [
    path('auth/', include(auth_urls)),
]

Конечная точка list работает нормально (например, /auth/tokens/6f82e9bc-46b8-4719-b99f-2dc0da062a02/domain_policies/); она возвращает список сериализованных объектов TokenDomainPolicy.

Однако, допустим, существует объект Domain с name = 'test.net', связанный с этим Token. Я бы подумал, что могу GET /auth/tokens/6f82e9bc-46b8-4719-b99f-2dc0da062a02/domain_policies/test.net/, чтобы получить этот объект, но результатом будет 404.

Дополнительные наблюдения:

  • Это работает , если я устанавливаю lookup_field = 'domain'. Однако это приводит к URL, содержащим ID Domain (например, .../25/), чего я не хочу. Но на основании этого я делаю вывод, что конечная точка -detail в принципе маршрутизируется.

  • Это работает , если я добавлю явное переопределение, например,

      path('tokens/<id>/domain_policies/<domain__name>/', views.TokenDomainPolicyViewSet.as_view(
          {'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'}
      ), name='token_domain_policies-detail'),
    

    Однако, почему обязательно явно связывать методы таким образом? (Это не обязательно, если lookup_field не задает поиск по иностранному ключу!)

  • Самое странное, что если я установлю django_extensions и запущу manage.py show_urls, то URL конечной точки -detail отображается с правильным URL kwarg <domain__name>, даже без переопределения из предыдущего пункта. Если я добавляю переопределение, то соответствующая строка в выводе отображается как идентичный дубликат.

    Как может быть так, что набор известных URL остается неизменным как с переопределением, так и без него, но в одном случае конечная точка работает как ожидалось, а в другом случае ответ 404?

  • Что я упускаю?

    Согласно docs, поиск соответствия по умолчанию игнорирует слеши и символы точки, поэтому test.name не может быть найден:

    Маршрутизатор будет сопоставлять значения поиска, содержащие любые символы, кроме косой черты и точки.

    Вы можете найти его также в source:

    lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
    

    Чтобы исправить, просто измените lookup_value_regex, чтобы разрешить периоды в поиске набора представлений:

    class TokenDomainPolicyViewSet(viewsets.ModelViewSet):
        lookup_field = 'domain__name'
        lookup_value_regex = '[^/]+'
    
    Вернуться на верх