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 в представлениях.

Вернуться на верх