Merge pull request #2010 from tanwanirahul/master

Ability to customize method names without creating a custom router
This commit is contained in:
Tom Christie 2014-12-19 16:09:01 +00:00
commit d109ae0a2e
4 changed files with 56 additions and 12 deletions

View File

@ -68,6 +68,24 @@ The following URL pattern would additionally be generated:
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'` * 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]. For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
# API Guide # API Guide

View File

@ -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. 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 ## Binding ViewSets to URLs explicitly
The handler methods only get bound to the actions when we define the URLConf. The handler methods only get bound to the actions when we define the URLConf.

View File

@ -176,23 +176,27 @@ class SimpleRouter(BaseRouter):
if isinstance(route, DynamicDetailRoute): if isinstance(route, DynamicDetailRoute):
# Dynamic detail routes (@detail_route decorator) # Dynamic detail routes (@detail_route decorator)
for httpmethods, methodname in detail_routes: 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 = route.initkwargs.copy()
initkwargs.update(getattr(viewset, methodname).kwargs) initkwargs.update(method_kwargs)
ret.append(Route( ret.append(Route(
url=replace_methodname(route.url, methodname), url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
name=replace_methodname(route.name, methodname), name=replace_methodname(route.name, url_path),
initkwargs=initkwargs, initkwargs=initkwargs,
)) ))
elif isinstance(route, DynamicListRoute): elif isinstance(route, DynamicListRoute):
# Dynamic list routes (@list_route decorator) # Dynamic list routes (@list_route decorator)
for httpmethods, methodname in list_routes: 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 = route.initkwargs.copy()
initkwargs.update(getattr(viewset, methodname).kwargs) initkwargs.update(method_kwargs)
ret.append(Route( ret.append(Route(
url=replace_methodname(route.url, methodname), url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
name=replace_methodname(route.name, methodname), name=replace_methodname(route.name, url_path),
initkwargs=initkwargs, initkwargs=initkwargs,
)) ))
else: else:

View File

@ -8,6 +8,7 @@ from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter from rest_framework.routers import SimpleRouter, DefaultRouter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from collections import namedtuple
factory = APIRequestFactory() factory = APIRequestFactory()
@ -261,6 +262,14 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet):
def detail_route_get(self, request, *args, **kwargs): def detail_route_get(self, request, *args, **kwargs):
return Response({'method': 'link2'}) 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): class TestDynamicListAndDetailRouter(TestCase):
def setUp(self): def setUp(self):
@ -269,22 +278,33 @@ class TestDynamicListAndDetailRouter(TestCase):
def test_list_and_detail_route_decorators(self): def test_list_and_detail_route_decorators(self):
routes = self.router.get_routes(DynamicListAndDetailViewSet) routes = self.router.get_routes(DynamicListAndDetailViewSet)
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] 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 # 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] route = decorator_routes[i]
# check url listing # 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, self.assertEqual(route.url,
'^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint)) '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
else: else:
self.assertEqual(route.url, self.assertEqual(route.url,
'^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint)) '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
# check method to function mapping # check method to function mapping
if endpoint.endswith('_post'): if method_name.endswith('_post'):
method_map = 'post' method_map = 'post'
else: else:
method_map = 'get' method_map = 'get'
self.assertEqual(route.mapping[method_map], endpoint) self.assertEqual(route.mapping[method_map], method_name)
class TestRootWithAListlessViewset(TestCase): class TestRootWithAListlessViewset(TestCase):