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?
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 = '[^/]+'