Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to customize router URLs for custom actions, using url_path. #2010

Merged
merged 9 commits into from
Dec 19, 2014
18 changes: 18 additions & 0 deletions docs/api-guide/routers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/tutorial/6-viewsets-and-routers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 10 additions & 6 deletions rest_framework/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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:
Expand Down
32 changes: 26 additions & 6 deletions tests/test_routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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(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):
Expand All @@ -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 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):
Expand Down