reverse on @list_route with custom url_path

2019-04-21 11:28发布

问题:

If I have a viewset with the following code:

class ExtraRouteViewset(viewsets.GenericViewSet):
    @list_route(methods=['get'])
    def somefunction(self, request):
        return Response({
            'key': 'value',
            'reverse': reverse('extraroute-somefunction'),
        })

    @list_route(methods=['get'], url_path='arguments/(?P<thing>[^/]+)')
    def arguments(self, request, thing):
        return Response({
            'key': thing,
            'reverse': reverse('extraroute-arguments', kwargs={'thing': 'something'}),
        })

I would expect both methods to work. However, the second reverse raises a NoReverseMatch. Examining the url patterns (by navigating to a non-existing url) shows the following url patterns:

^demo/ ^ ^extraroute/arguments/(?P<thing>[^/]+)/$ [name='extraroute-arguments/(?P<thing>[^/]+)']
^demo/ ^ ^extraroute/arguments/(?P<thing>[^/]+)/\.(?P<format>[a-z0-9]+)$ [name='extraroute-arguments/(?P<thing>[^/]+)']
^demo/ ^ ^extraroute/somefunction/$ [name='extraroute-somefunction']
^demo/ ^ ^extraroute/somefunction/\.(?P<format>[a-z0-9]+)$ [name='extraroute-somefunction']

The view name seems to be extraroute-arguments/(?P<thing>[^/]+) instead of extraroute-arguments? And indeed, if I use reverse('extraroute-arguments/(?P<thing>[^/]+)', kwargs={'thing': 'something'}) it works. Am I missing something very obvious here, or is this a bug in django-rest-framework?

This is using Django 1.8a and django-rest-framework 3.0.5.

回答1:

Well, in the second example, you send url_path='arguments/(?P<thing>[^/]+)'. Django REST framework use it to create both an URL pattern and a URL Name. But the implementation is too pure to strip the regex expression.

Solution with a custom router

#inside urls.py
router = SimpleRouter()
router.routes.append(
    Route(
        url=r'^{prefix}/arguments/(?P<thing>[^/]+)$',
        name='{basename}-arguments',
        mapping={
            'get': 'arguments',
        },
        initkwargs={}
    ),
)
router.register('extraroute', ExtraRouteViewset, base_name='extraroute')
urlpatterns = router.urls

and then in the views.py remove the @list_route decorators since its no more needed (and will cause a route clash)

#inside views.py
class ExtraRouteViewset(viewsets.GenericViewSet):
    #...

    def arguments(self, request, thing):
        return Response({
            'key': thing,
            'reverse': reverse('extraroute-arguments', kwargs={'thing': 'something'}),
        })

I have to mention that this actually adds a hardcoded Route pattern inside the default SimpleRouter (which has patterns for list, create, retrieve, update, partial update, destroy). This means that every viewset which get registered via this router instance will be able to implement an arguments method and this method will be called when the regex match it.