Search code examples
pythondjangodjango-rest-frameworkdjango-rest-viewsets

DRF: router for ViewSet with a foreign-key lookup_field


Using Django REST Framework 3.12.4, I cannot properly get the URLs for a ViewSet to work if that ViewSet has a foreign-key lookup field.

I have the following in 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)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    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')]

In views.py, I have:

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

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

You can see from TokenDomainPolicyViewSet.lookup_field that I would like to be able to query the -detail endpoint by using the related Domain's name field instead of its primary key. (name is unique for a given token.)

I thought this can be done with lookup_field = 'domain__name'.

However, it doesn't quite work. Here's my 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)),
]

The list endpoint works fine (e.g. /auth/tokens/6f82e9bc-46b8-4719-b99f-2dc0da062a02/domain_policies/); it returns a list of serialized TokenDomainPolicy objects.

However, let's say there is a Domain object with name = 'test.net' related to this Token. I would think I can GET /auth/tokens/6f82e9bc-46b8-4719-b99f-2dc0da062a02/domain_policies/test.net/ to retrieve this object, but the result is 404.

Additional observations:

  • It nearly does work if I set lookup_field = 'domain'. However, this leads to URLs that contain the Domain's ID (like .../25/) which is not what I want. But based on this, I conclude that the -detail endpoint does get routed in principle.

  • It does work if I add an explicit override like

      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'),
    

    However, why would it be necessarily to explicitly bind the methods like this? (It's not necessary if lookup_field does not specify a foreign-key lookup!)

  • Weirdest of all, if I install django_extensions and run manage.py show_urls, the -detail endpoint URL shows up with the correct <domain__name> URL kwarg, even without the override from the previous bullet. If I add the override, the corresponding line in the output is displayed twice as an identical duplicate.

    How can it be that the set of known URLs remains unchanged with or without the override, but in one case the endpoint works as expected, and in the other case the response is 404?

What am I missing?


Solution

  • According to the docs, the default match lookup will ignore slashes and period characters, that's why test.name can't be found:

    The router will match lookup values containing any characters except slashes and period characters.

    You can find it in the source as well:

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

    So to fix, just change the lookup_value_regex to allow periods in the viewset's lookup:

    class TokenDomainPolicyViewSet(viewsets.ModelViewSet):
        lookup_field = 'domain__name'
        lookup_value_regex = '[^/]+'