10

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 Answer 1

8

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.

Sign up to request clarification or add additional context in comments.

5 Comments

So, it could be classified as a bug then? The replace_methodname doesn't really work on these url paths, it would be nice if there would be an argument to supply the view name. I'll see if I can come up with a patch.
Yep, that would be very helpfull :)
After reviewing the question again i have to agree with maryokhin on GitHub, nested routing on drf-extensions really seams to be the more proper way to go. For completeness and correctness of my answer i'll try to edit it and add a solution with nested/custom routing.
OK, an example solution added.
@Todor, i think the actual solution is to add an endpoint argument for DRF's list_route and detail_route. It's here in DRF Extensions but the doc said the action decorator is going to be deprecated soon.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.