Reputation: 330
I noticed that sites such as SO and Reddit use the url structure <basename>/<pk>/<slug>/
to route to the detail view of their posts, which makes me think that this should be the standard. Is there a way to accomplish this with django-rest-framework using DefaultRouter
and ModelViewset
?
example views.py
:
class PostViewSet(viewsets.ModelViewSet):
...
lookup_fields = ['pk', 'slug']
example urls.py
:
router = DefaultRouter()
router.register('posts', PostViewSet, basename='post')
app_name = 'api'
urlpatterns = [
path('', include(router.urls)),
]
URL routes:
/api/post/ api.views.PostViewSet api:post-list
/api/post/<pk>/<slug>/ api.views.PostViewSet api:post-detail
/api/post/<pk>/<slug>\.<format>/ api.views.PostViewSet api:post-detail
/api/post\.<format>/ api.views.PostViewSet api:post-list
Upvotes: 1
Views: 1600
Reputation: 330
I dove into the source code of DefaultRouter
and came up with a solution that passed my unittests.
create a file routers.py
from rest_framework.routers import DefaultRouter
class CustomRouter(DefaultRouter):
def get_lookup_regex(self, viewset, lookup_prefix=''):
combined_lookup_field = getattr(viewset, 'combined_lookup_field', None)
if combined_lookup_field:
multi_base_regex_list = []
for lookup_field in combined_lookup_field:
base_regex = '(?P<{lookup_prefix}>{lookup_value})'
lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
multi_base_regex_list.append(
base_regex.format(
lookup_prefix=lookup_field,
lookup_value=lookup_value
)
)
return'/'.join(multi_base_regex_list)
return super().get_lookup_regex(viewset, lookup_prefix)
replace lookup_field
or add the following in views.py
class PostViewSet(viewsets.ModelViewSet):
...
combined_lookup_field = ['pk', 'slug']
replace DefaultRouter
with CustomRouter
in urls.py
from .routers import CustomRouter
router = CustomRouter()
router.register('posts', PostViewSet, basename='post')
app_name = 'api'
urlpatterns = [
path('', include(router.urls)),
]
And that's it! It still incorporates all other functionality of DefaultRouter and will only use the added logic on views that have combined_lookup_field
defined. It also supports @action
functionality in the views.
Upvotes: 0
Reputation: 40941
You can use the MultipleLookupField mixin strategy and define a custom get_object
method on your viewset for this.
class PostViewSet(viewsets.ModelViewSet):
lookup_fields = ['pk', 'slug']
# ...
def get_object(self):
if all(arg in self.kwargs for arg in self.lookup_fields):
# detected the custom URL pattern; return the specified object
qs = self.get_queryset()
qs_filter = {field: self.kwargs[field] for field in self.lookup_fields}
obj = get_object_or_404(qs, **qs_filter)
self.check_object_permissions(self.request, obj)
return obj
else: # missing lookup fields
raise InvalidQueryError("Missing fields")
class InvalidQueryError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
In this case, I just override get_object
directly on the viewset. But you can make this a mixin class instead in order to easily include it into other viewsets.
The default router, however, will not automatically add this URL pattern to your app, so you'll have to do that manually.
urlpatterns = [
...,
path('api/post/<int:pk>/<str:slug>', views.PostViewSet.as_view()),
]
Upvotes: 1