aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/api-guide/routers.md18
-rw-r--r--docs/tutorial/6-viewsets-and-routers.md2
-rw-r--r--rest_framework/routers.py16
-rw-r--r--tests/test_routers.py32
4 files changed, 56 insertions, 12 deletions
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 929a1710..6819adb6 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -68,6 +68,24 @@ The following URL pattern would additionally be generated:
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
+If you do not want to use the default URL generated for your custom action, you can instead use the url_path parameter to customize it.
+
+For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write:
+
+ from myapp.permissions import IsAdminOrIsSelf
+ from rest_framework.decorators import detail_route
+
+ class UserViewSet(ModelViewSet):
+ ...
+
+ @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_path='change-password')
+ def set_password(self, request, pk=None):
+ ...
+
+The above example would now generate the following URL pattern:
+
+* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
+
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
# API Guide
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index d55a60de..63dff73f 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -53,6 +53,8 @@ Notice that we've also used the `@detail_route` decorator to create a custom act
Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests.
+The URLs for custom actions by default depend on the method name itself. If you want to change the way url should be constructed, you can include url_path as a decorator keyword argument.
+
## Binding ViewSets to URLs explicitly
The handler methods only get bound to the actions when we define the URLConf.
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 6e99f14d..1cb65b1c 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -176,23 +176,27 @@ class SimpleRouter(BaseRouter):
if isinstance(route, DynamicDetailRoute):
# 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(getattr(viewset, methodname).kwargs)
+ initkwargs.update(method_kwargs)
ret.append(Route(
- url=replace_methodname(route.url, methodname),
+ url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, methodname),
+ name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
elif isinstance(route, DynamicListRoute):
# 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(getattr(viewset, methodname).kwargs)
+ initkwargs.update(method_kwargs)
ret.append(Route(
- url=replace_methodname(route.url, methodname),
+ url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, methodname),
+ name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
else:
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 34306146..06ab8103 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -8,6 +8,7 @@ from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter
from rest_framework.test import APIRequestFactory
+from collections import namedtuple
factory = APIRequestFactory()
@@ -261,6 +262,14 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet):
def detail_route_get(self, request, *args, **kwargs):
return Response({'method': 'link2'})
+ @list_route(url_path="list_custom-route")
+ def list_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route(url_path="detail_custom-route")
+ def detail_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
class TestDynamicListAndDetailRouter(TestCase):
def setUp(self):
@@ -269,22 +278,33 @@ class TestDynamicListAndDetailRouter(TestCase):
def test_list_and_detail_route_decorators(self):
routes = self.router.get_routes(DynamicListAndDetailViewSet)
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')
# Make sure all these endpoints exist and none have been clobbered
- for i, endpoint in enumerate(['list_route_get', 'list_route_post', 'detail_route_get', 'detail_route_post']):
+ for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'),
+ MethodNamesMap('list_route_get', 'list_route_get'),
+ MethodNamesMap('list_route_post', 'list_route_post'),
+ MethodNamesMap('detail_custom_route_get', 'detail_custom-route'),
+ MethodNamesMap('detail_route_get', 'detail_route_get'),
+ MethodNamesMap('detail_route_post', 'detail_route_post')
+ ]):
route = decorator_routes[i]
# check url listing
- if endpoint.startswith('list_'):
+ method_name = endpoint.method_name
+ url_path = endpoint.url_path
+
+ if method_name.startswith('list_'):
self.assertEqual(route.url,
- '^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint))
+ '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
else:
self.assertEqual(route.url,
- '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
# check method to function mapping
- if endpoint.endswith('_post'):
+ if method_name.endswith('_post'):
method_map = 'post'
else:
method_map = 'get'
- self.assertEqual(route.mapping[method_map], endpoint)
+ self.assertEqual(route.mapping[method_map], method_name)
class TestRootWithAListlessViewset(TestCase):