Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

reverse on @list_route with custom url_path

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.

like image 471
Michel Avatar asked Mar 03 '15 20:03

Michel


1 Answers

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.

like image 70
Todor Avatar answered Sep 21 '22 15:09

Todor