From d972df7c9c1867b4a0a57307f423a488c4d4f4b1 Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Mon, 3 Nov 2014 14:43:53 +0100 Subject: [PATCH 1/8] Ability to override default method names by customizing it --- rest_framework/routers.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 169e6e8bc4..d1c9fa1b91 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -176,23 +176,27 @@ def get_routes(self, viewset): if isinstance(route, DynamicDetailRoute): # Dynamic detail routes (@detail_route decorator) for httpmethods, methodname in detail_routes: + method_kwargs = getattr(viewset, methodname).kwargs + custom_method_name = method_kwargs.pop("custom_method_name", 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, custom_method_name), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, methodname), + name=replace_methodname(route.name, custom_method_name), initkwargs=initkwargs, )) elif isinstance(route, DynamicListRoute): # Dynamic list routes (@list_route decorator) for httpmethods, methodname in list_routes: + method_kwargs = getattr(viewset, methodname).kwargs + custom_method_name = method_kwargs.pop("custom_method_name", 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, custom_method_name), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, methodname), + name=replace_methodname(route.name, custom_method_name), initkwargs=initkwargs, )) else: From ea8c40520165fc33343fceb15221b770701bdedf Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Mon, 3 Nov 2014 14:44:47 +0100 Subject: [PATCH 2/8] Tests for validating custom_method_name router attribute --- tests/test_routers.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index f6f5a977a5..d426f83206 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -8,6 +8,7 @@ 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() @@ -260,6 +261,14 @@ def list_route_get(self, request, *args, **kwargs): def detail_route_get(self, request, *args, **kwargs): return Response({'method': 'link2'}) + @list_route(custom_method_name="list_custom-route") + def list_custom_route_get(self, request, *args, **kwargs): + return Response({'method': 'link1'}) + + @detail_route(custom_method_name="detail_custom-route") + def detail_custom_route_get(self, request, *args, **kwargs): + return Response({'method': 'link2'}) + class TestDynamicListAndDetailRouter(TestCase): def setUp(self): @@ -268,22 +277,33 @@ def setUp(self): 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 custom_method_name') # 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 + custom_method_name = endpoint.custom_method_name + + if method_name.startswith('list_'): self.assertEqual(route.url, - '^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint)) + '^{{prefix}}/{0}{{trailing_slash}}$'.format(custom_method_name)) else: self.assertEqual(route.url, - '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint)) + '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(custom_method_name)) # 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): From 92ebeaa040f75dbc6142355fa25d89b4c990685b Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Fri, 19 Dec 2014 19:52:59 +0530 Subject: [PATCH 3/8] Change decorator attribute name to url_path per suggestions --- rest_framework/routers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index d1c9fa1b91..a213f62c71 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -177,26 +177,26 @@ def get_routes(self, viewset): # Dynamic detail routes (@detail_route decorator) for httpmethods, methodname in detail_routes: method_kwargs = getattr(viewset, methodname).kwargs - custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname + url_path = method_kwargs.pop("url_path", None) or methodname initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) ret.append(Route( - url=replace_methodname(route.url, custom_method_name), + url=replace_methodname(route.url, url_path), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, custom_method_name), + 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 - custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname + url_path = method_kwargs.pop("url_path", None) or methodname initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) ret.append(Route( - url=replace_methodname(route.url, custom_method_name), + url=replace_methodname(route.url, url_path), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, custom_method_name), + name=replace_methodname(route.name, url_path), initkwargs=initkwargs, )) else: From 2448cc8e856369ca6fb99b848e10f8ff0105e925 Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Fri, 19 Dec 2014 19:53:48 +0530 Subject: [PATCH 4/8] Updated tests to use url_path attribute in list and detail decorators --- tests/test_routers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index d426f83206..73d10822a0 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -261,11 +261,11 @@ def list_route_get(self, request, *args, **kwargs): def detail_route_get(self, request, *args, **kwargs): return Response({'method': 'link2'}) - @list_route(custom_method_name="list_custom-route") + @list_route(url_path="list_custom-route") def list_custom_route_get(self, request, *args, **kwargs): return Response({'method': 'link1'}) - @detail_route(custom_method_name="detail_custom-route") + @detail_route(url_path="detail_custom-route") def detail_custom_route_get(self, request, *args, **kwargs): return Response({'method': 'link2'}) @@ -278,7 +278,7 @@ 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 custom_method_name') + MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path') # Make sure all these endpoints exist and none have been clobbered for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'), MethodNamesMap('list_route_get', 'list_route_get'), @@ -290,14 +290,14 @@ def test_list_and_detail_route_decorators(self): route = decorator_routes[i] # check url listing method_name = endpoint.method_name - custom_method_name = endpoint.custom_method_name + url_path = endpoint.url_path if method_name.startswith('list_'): self.assertEqual(route.url, - '^{{prefix}}/{0}{{trailing_slash}}$'.format(custom_method_name)) + '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path)) else: self.assertEqual(route.url, - '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(custom_method_name)) + '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path)) # check method to function mapping if method_name.endswith('_post'): method_map = 'post' From a8a3fedb5c52cc62c6ecf59c4138e9a6ecf04806 Mon Sep 17 00:00:00 2001 From: tanwanirahul Date: Fri, 19 Dec 2014 20:16:46 +0530 Subject: [PATCH 5/8] Add url_path documention for detail_route decorator --- docs/tutorial/6-viewsets-and-routers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index cf37a26016..8e4e22f050 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 depends on the method name itself. If you want to change the way url should be constructed, you can use `url_path` parameter of `@detail_route` and provide the string value for the same. + ## Binding ViewSets to URLs explicitly The handler methods only get bound to the actions when we define the URLConf. From 6aa0e307c99d0c17d7c48f2416472c7dbdcbbf8f Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 19 Dec 2014 20:31:21 +0530 Subject: [PATCH 6/8] Added documentation about url_path parameter for custom actions. --- docs/api-guide/routers.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 61a476b8bb..63b8b59a82 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 did not like the default URL generated for your custom action, you could use `url_path` parameter with `@detail_route` or `@list_route` 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): + ... + +Above example would instead generate 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 From b4a3e7f64096ea7106ff0d622bdf1c6e2e4e2895 Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 19 Dec 2014 21:20:19 +0530 Subject: [PATCH 7/8] Updates url_path info per suggestion --- docs/api-guide/routers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 63b8b59a82..87b6f15ac5 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -68,7 +68,7 @@ The following URL pattern would additionally be generated: * URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'` -If you did not like the default URL generated for your custom action, you could use `url_path` parameter with `@detail_route` or `@list_route` to customize it. +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: @@ -82,7 +82,7 @@ For example, if you want to change the URL for our custom action to `^users/{pk} def set_password(self, request, pk=None): ... -Above example would instead generate following URL pattern: +The above example would now generate the following URL pattern: * URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'` From 8f0fef4b75f5c999c13b5d37a263da3a3388142e Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 19 Dec 2014 21:22:10 +0530 Subject: [PATCH 8/8] Updated documentation on url_path per suggestions. --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 8e4e22f050..d2ee110282 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -53,7 +53,7 @@ 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 depends on the method name itself. If you want to change the way url should be constructed, you can use `url_path` parameter of `@detail_route` and provide the string value for the same. +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