Repository: encode/django-rest-framework

Issue:
url_path feature does not work for nested routes
As per discussion on IRC with @tomchristie and chibisov/drf-extensions#70, the current <a href="https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/routers.py#L174">popping out</a> of the `url_path` kwarg does not work for downstream packages that reuse the same viewset for constructing nested routes. The reason for that is `get_routes()` being called multiple times (our router only calls it once) for each subsequent nested route, and therefore only the first route will know that `url_path` argument was set.

P.S. As an adhoc example solution, <a href="https://gist.github.com/maryokhin/8765b07aa335b4d4bbcd#file-routers-py-L60">popping back in</a> `url_path` if it diverges from the method name on subsequent routes.



Discussion:
As a side note, why is the code in the `if` blocks duplicated and not i.e. a private method? 

``` python
if isinstance(route, DynamicDetailRoute):
    ...
elif isinstance(route, DynamicListRoute):
    ...
```

This is also an issue if one has a mixin with a `list_route` or a `detail_route` that is used in several place.

I'm working on a PR.


Resolving patch:
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -171,9 +171,9 @@ def get_routes(self, viewset):
                 # Dynamic detail routes (@detail_route decorator)
                 for httpmethods, methodname in detail_routes:
                     method_kwargs = getattr(viewset, methodname).kwargs
-                    url_path = method_kwargs.pop("url_path", None) or methodname
                     initkwargs = route.initkwargs.copy()
                     initkwargs.update(method_kwargs)
+                    url_path = initkwargs.pop("url_path", None) or methodname
                     ret.append(Route(
                         url=replace_methodname(route.url, url_path),
                         mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
@@ -184,9 +184,9 @@ def get_routes(self, viewset):
                 # Dynamic list routes (@list_route decorator)
                 for httpmethods, methodname in list_routes:
                     method_kwargs = getattr(viewset, methodname).kwargs
-                    url_path = method_kwargs.pop("url_path", None) or methodname
                     initkwargs = route.initkwargs.copy()
                     initkwargs.update(method_kwargs)
+                    url_path = initkwargs.pop("url_path", None) or methodname
                     ret.append(Route(
                         url=replace_methodname(route.url, url_path),
                         mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),


Test patch:
diff --git a/tests/test_routers.py b/tests/test_routers.py
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -302,12 +302,16 @@ def detail_custom_route_get(self, request, *args, **kwargs):
         return Response({'method': 'link2'})
 
 
+class SubDynamicListAndDetailViewSet(DynamicListAndDetailViewSet):
+    pass
+
+
 class TestDynamicListAndDetailRouter(TestCase):
     def setUp(self):
         self.router = SimpleRouter()
 
-    def test_list_and_detail_route_decorators(self):
-        routes = self.router.get_routes(DynamicListAndDetailViewSet)
+    def _test_list_and_detail_route_decorators(self, viewset):
+        routes = self.router.get_routes(viewset)
         decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
 
         MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
@@ -336,3 +340,9 @@ def test_list_and_detail_route_decorators(self):
             else:
                 method_map = 'get'
             self.assertEqual(route.mapping[method_map], method_name)
+
+    def test_list_and_detail_route_decorators(self):
+        self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet)
+
+    def test_inherited_list_and_detail_route_decorators(self):
+        self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet)