Skip to content

Commit d109ae0

Browse files
committed
Merge pull request #2010 from tanwanirahul/master
Ability to customize method names without creating a custom router
2 parents 80bacc5 + 8f0fef4 commit d109ae0

File tree

4 files changed

+56
-12
lines changed

4 files changed

+56
-12
lines changed

docs/api-guide/routers.md

+18
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ The following URL pattern would additionally be generated:
6868

6969
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
7070

71+
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.
72+
73+
For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write:
74+
75+
from myapp.permissions import IsAdminOrIsSelf
76+
from rest_framework.decorators import detail_route
77+
78+
class UserViewSet(ModelViewSet):
79+
...
80+
81+
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_path='change-password')
82+
def set_password(self, request, pk=None):
83+
...
84+
85+
The above example would now generate the following URL pattern:
86+
87+
* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
88+
7189
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
7290

7391
# API Guide

docs/tutorial/6-viewsets-and-routers.md

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ Notice that we've also used the `@detail_route` decorator to create a custom act
5353

5454
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.
5555

56+
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.
57+
5658
## Binding ViewSets to URLs explicitly
5759

5860
The handler methods only get bound to the actions when we define the URLConf.

rest_framework/routers.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -176,23 +176,27 @@ def get_routes(self, viewset):
176176
if isinstance(route, DynamicDetailRoute):
177177
# Dynamic detail routes (@detail_route decorator)
178178
for httpmethods, methodname in detail_routes:
179+
method_kwargs = getattr(viewset, methodname).kwargs
180+
url_path = method_kwargs.pop("url_path", None) or methodname
179181
initkwargs = route.initkwargs.copy()
180-
initkwargs.update(getattr(viewset, methodname).kwargs)
182+
initkwargs.update(method_kwargs)
181183
ret.append(Route(
182-
url=replace_methodname(route.url, methodname),
184+
url=replace_methodname(route.url, url_path),
183185
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
184-
name=replace_methodname(route.name, methodname),
186+
name=replace_methodname(route.name, url_path),
185187
initkwargs=initkwargs,
186188
))
187189
elif isinstance(route, DynamicListRoute):
188190
# Dynamic list routes (@list_route decorator)
189191
for httpmethods, methodname in list_routes:
192+
method_kwargs = getattr(viewset, methodname).kwargs
193+
url_path = method_kwargs.pop("url_path", None) or methodname
190194
initkwargs = route.initkwargs.copy()
191-
initkwargs.update(getattr(viewset, methodname).kwargs)
195+
initkwargs.update(method_kwargs)
192196
ret.append(Route(
193-
url=replace_methodname(route.url, methodname),
197+
url=replace_methodname(route.url, url_path),
194198
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
195-
name=replace_methodname(route.name, methodname),
199+
name=replace_methodname(route.name, url_path),
196200
initkwargs=initkwargs,
197201
))
198202
else:

tests/test_routers.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from rest_framework.response import Response
99
from rest_framework.routers import SimpleRouter, DefaultRouter
1010
from rest_framework.test import APIRequestFactory
11+
from collections import namedtuple
1112

1213
factory = APIRequestFactory()
1314

@@ -261,6 +262,14 @@ def list_route_get(self, request, *args, **kwargs):
261262
def detail_route_get(self, request, *args, **kwargs):
262263
return Response({'method': 'link2'})
263264

265+
@list_route(url_path="list_custom-route")
266+
def list_custom_route_get(self, request, *args, **kwargs):
267+
return Response({'method': 'link1'})
268+
269+
@detail_route(url_path="detail_custom-route")
270+
def detail_custom_route_get(self, request, *args, **kwargs):
271+
return Response({'method': 'link2'})
272+
264273

265274
class TestDynamicListAndDetailRouter(TestCase):
266275
def setUp(self):
@@ -269,22 +278,33 @@ def setUp(self):
269278
def test_list_and_detail_route_decorators(self):
270279
routes = self.router.get_routes(DynamicListAndDetailViewSet)
271280
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
281+
282+
MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
272283
# Make sure all these endpoints exist and none have been clobbered
273-
for i, endpoint in enumerate(['list_route_get', 'list_route_post', 'detail_route_get', 'detail_route_post']):
284+
for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'),
285+
MethodNamesMap('list_route_get', 'list_route_get'),
286+
MethodNamesMap('list_route_post', 'list_route_post'),
287+
MethodNamesMap('detail_custom_route_get', 'detail_custom-route'),
288+
MethodNamesMap('detail_route_get', 'detail_route_get'),
289+
MethodNamesMap('detail_route_post', 'detail_route_post')
290+
]):
274291
route = decorator_routes[i]
275292
# check url listing
276-
if endpoint.startswith('list_'):
293+
method_name = endpoint.method_name
294+
url_path = endpoint.url_path
295+
296+
if method_name.startswith('list_'):
277297
self.assertEqual(route.url,
278-
'^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint))
298+
'^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
279299
else:
280300
self.assertEqual(route.url,
281-
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
301+
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
282302
# check method to function mapping
283-
if endpoint.endswith('_post'):
303+
if method_name.endswith('_post'):
284304
method_map = 'post'
285305
else:
286306
method_map = 'get'
287-
self.assertEqual(route.mapping[method_map], endpoint)
307+
self.assertEqual(route.mapping[method_map], method_name)
288308

289309

290310
class TestRootWithAListlessViewset(TestCase):

0 commit comments

Comments
 (0)