DRF DefaultRouter pk/slug
Я заметил, что такие сайты, как SO и Reddit, используют структуру url <basename>/<pk>/<slug>/ для перехода к детальному просмотру своих постов, что заставляет меня думать, что это должно быть стандартом. Есть ли способ сделать это с помощью django-rest-framework, используя DefaultRouter и ModelViewset?
пример views.py:
class PostViewSet(viewsets.ModelViewSet):
...
lookup_fields = ['pk', 'slug']
пример urls.py :
router = DefaultRouter()
router.register('posts', PostViewSet, basename='post')
app_name = 'api'
urlpatterns = [
path('', include(router.urls)),
]
Шаблоны URL:
/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
Вы можете использовать стратегию смешивания MultipleLookupField и определить пользовательский get_object метод в вашем наборе представлений для этого.
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
В данном случае я просто переопределяю get_object непосредственно в наборе представлений. Но вы можете сделать этот класс микшированным, чтобы легко включать его в другие наборы представлений.
Однако маршрутизатор по умолчанию не будет автоматически добавлять этот шаблон URL в ваше приложение, поэтому вам придется сделать это вручную.
urlpatterns = [
...,
path('api/post/<int:pk>/<str:slug>', views.PostViewSet.as_view()),
]
Я погрузился в исходный код DefaultRouter и придумал решение, которое прошло мои unittests.
Исходный код маршрутизаторов DDRF
создайте файл 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', '[^/.]+')
base_regex = base_regex.format(
lookup_prefix=lookup_field,
lookup_value=lookup_value
)
multi_base_regex_list.append(base_regex)
return '/'.join(multi_base_regex_list)
return super().get_lookup_regex(viewset, lookup_prefix)
замените lookup_field или добавьте следующее в views.py
class PostViewSet(viewsets.ModelViewSet):
...
combined_lookup_field = ['pk', 'slug']
замените DefaultRouter на CustomRouter в urls.py
from .routers import CustomRouter
router = CustomRouter()
router.register('posts', PostViewSet, basename='post')
app_name = 'api'
urlpatterns = [
path('', include(router.urls)),
]
И это все! Он по-прежнему включает в себя всю остальную функциональность DefaultRouter и будет использовать добавленную логику только в представлениях, для которых определено combined_lookup_field. Он также поддерживает функциональность @action в представлениях.