aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/decorators.py37
-rw-r--r--rest_framework/routers.py54
-rw-r--r--rest_framework/tests/test_routers.py64
3 files changed, 128 insertions, 27 deletions
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index c69756a4..1ca176f2 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -3,13 +3,14 @@ The most important decorator in this module is `@api_view`, which is used
for writing function-based views with REST framework.
There are also various decorators for setting the API policies on function
-based views, as well as the `@action` and `@link` decorators, which are
+based views, as well as the `@detail_route` and `@list_route` decorators, which are
used to annotate methods on viewsets that should be included by routers.
"""
from __future__ import unicode_literals
from rest_framework.compat import six
from rest_framework.views import APIView
import types
+import warnings
def api_view(http_method_names):
@@ -109,10 +110,13 @@ def permission_classes(permission_classes):
def link(**kwargs):
"""
- Used to mark a method on a ViewSet that should be routed for GET requests.
+ Used to mark a method on a ViewSet that should be routed for detail GET requests.
"""
+ msg = 'link is pending deprecation. Use detail_route instead.'
+ warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = ['get']
+ func.detail = True
func.kwargs = kwargs
return func
return decorator
@@ -120,10 +124,37 @@ def link(**kwargs):
def action(methods=['post'], **kwargs):
"""
- Used to mark a method on a ViewSet that should be routed for POST requests.
+ Used to mark a method on a ViewSet that should be routed for detail POST requests.
"""
+ msg = 'action is pending deprecation. Use detail_route instead.'
+ warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = methods
+ func.detail = True
+ func.kwargs = kwargs
+ return func
+ return decorator
+
+
+def detail_route(methods=['get'], **kwargs):
+ """
+ Used to mark a method on a ViewSet that should be routed for detail requests.
+ """
+ def decorator(func):
+ func.bind_to_methods = methods
+ func.detail = True
+ func.kwargs = kwargs
+ return func
+ return decorator
+
+
+def list_route(methods=['get'], **kwargs):
+ """
+ Used to mark a method on a ViewSet that should be routed for list requests.
+ """
+ def decorator(func):
+ func.bind_to_methods = methods
+ func.detail = False
func.kwargs = kwargs
return func
return decorator
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 930011d3..b761ba9a 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -26,6 +26,8 @@ from rest_framework.urlpatterns import format_suffix_patterns
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
+DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwargs'])
+DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs'])
def replace_methodname(format_string, methodname):
@@ -88,6 +90,14 @@ class SimpleRouter(BaseRouter):
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
+ # Dynamically generated list routes.
+ # Generated using @list_route decorator
+ # on methods of the viewset.
+ DynamicListRoute(
+ url=r'^{prefix}/{methodname}{trailing_slash}$',
+ name='{basename}-{methodnamehyphen}',
+ initkwargs={}
+ ),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
@@ -100,13 +110,10 @@ class SimpleRouter(BaseRouter):
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
),
- # Dynamically generated routes.
- # Generated using @action or @link decorators on methods of the viewset.
- Route(
+ # Dynamically generated detail routes.
+ # Generated using @detail_route decorator on methods of the viewset.
+ DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
- mapping={
- '{httpmethod}': '{methodname}',
- },
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
@@ -139,25 +146,42 @@ class SimpleRouter(BaseRouter):
Returns a list of the Route namedtuple.
"""
- known_actions = flatten([route.mapping.values() for route in self.routes])
+ known_actions = flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)])
- # Determine any `@action` or `@link` decorated methods on the viewset
- dynamic_routes = []
+ # Determine any `@detail_route` or `@list_route` decorated methods on the viewset
+ detail_routes = []
+ list_routes = []
for methodname in dir(viewset):
attr = getattr(viewset, methodname)
httpmethods = getattr(attr, 'bind_to_methods', None)
+ detail = getattr(attr, 'detail', True)
if httpmethods:
if methodname in known_actions:
- raise ImproperlyConfigured('Cannot use @action or @link decorator on '
- 'method "%s" as it is an existing route' % methodname)
+ raise ImproperlyConfigured('Cannot use @detail_route or @list_route '
+ 'decorators on method "%s" '
+ 'as it is an existing route' % methodname)
httpmethods = [method.lower() for method in httpmethods]
- dynamic_routes.append((httpmethods, methodname))
+ if detail:
+ detail_routes.append((httpmethods, methodname))
+ else:
+ list_routes.append((httpmethods, methodname))
ret = []
for route in self.routes:
- if route.mapping == {'{httpmethod}': '{methodname}'}:
- # Dynamic routes (@link or @action decorator)
- for httpmethods, methodname in dynamic_routes:
+ if isinstance(route, DynamicDetailRoute):
+ # Dynamic detail routes (@detail_route decorator)
+ for httpmethods, methodname in detail_routes:
+ initkwargs = route.initkwargs.copy()
+ initkwargs.update(getattr(viewset, methodname).kwargs)
+ ret.append(Route(
+ url=replace_methodname(route.url, methodname),
+ mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
+ name=replace_methodname(route.name, methodname),
+ initkwargs=initkwargs,
+ ))
+ elif isinstance(route, DynamicListRoute):
+ # Dynamic list routes (@list_route decorator)
+ for httpmethods, methodname in list_routes:
initkwargs = route.initkwargs.copy()
initkwargs.update(getattr(viewset, methodname).kwargs)
ret.append(Route(
diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py
index 5fcccb74..c3597e38 100644
--- a/rest_framework/tests/test_routers.py
+++ b/rest_framework/tests/test_routers.py
@@ -4,7 +4,7 @@ from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from rest_framework import serializers, viewsets, permissions
from rest_framework.compat import include, patterns, url
-from rest_framework.decorators import link, action
+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
@@ -18,23 +18,23 @@ class BasicViewSet(viewsets.ViewSet):
def list(self, request, *args, **kwargs):
return Response({'method': 'list'})
- @action()
+ @detail_route(methods=['post'])
def action1(self, request, *args, **kwargs):
return Response({'method': 'action1'})
- @action()
+ @detail_route(methods=['post'])
def action2(self, request, *args, **kwargs):
return Response({'method': 'action2'})
- @action(methods=['post', 'delete'])
+ @detail_route(methods=['post', 'delete'])
def action3(self, request, *args, **kwargs):
return Response({'method': 'action2'})
- @link()
+ @detail_route()
def link1(self, request, *args, **kwargs):
return Response({'method': 'link1'})
- @link()
+ @detail_route()
def link2(self, request, *args, **kwargs):
return Response({'method': 'link2'})
@@ -175,7 +175,7 @@ class TestActionKeywordArgs(TestCase):
class TestViewSet(viewsets.ModelViewSet):
permission_classes = []
- @action(permission_classes=[permissions.AllowAny])
+ @detail_route(methods=['post'], permission_classes=[permissions.AllowAny])
def custom(self, request, *args, **kwargs):
return Response({
'permission_classes': self.permission_classes
@@ -196,14 +196,14 @@ class TestActionKeywordArgs(TestCase):
class TestActionAppliedToExistingRoute(TestCase):
"""
- Ensure `@action` decorator raises an except when applied
+ Ensure `@detail_route` decorator raises an except when applied
to an existing route
"""
def test_exception_raised_when_action_applied_to_existing_route(self):
class TestViewSet(viewsets.ModelViewSet):
- @action()
+ @detail_route(methods=['post'])
def retrieve(self, request, *args, **kwargs):
return Response({
'hello': 'world'
@@ -214,3 +214,49 @@ class TestActionAppliedToExistingRoute(TestCase):
with self.assertRaises(ImproperlyConfigured):
self.router.urls
+
+
+class DynamicListAndDetailViewSet(viewsets.ViewSet):
+ def list(self, request, *args, **kwargs):
+ return Response({'method': 'list'})
+
+ @list_route(methods=['post'])
+ def list_route_post(self, request, *args, **kwargs):
+ return Response({'method': 'action1'})
+
+ @detail_route(methods=['post'])
+ def detail_route_post(self, request, *args, **kwargs):
+ return Response({'method': 'action2'})
+
+ @list_route()
+ def list_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route()
+ def detail_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
+
+class TestDynamicListAndDetailRouter(TestCase):
+ def setUp(self):
+ self.router = SimpleRouter()
+
+ 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'))]
+ # 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']):
+ route = decorator_routes[i]
+ # check url listing
+ if endpoint.startswith('list_'):
+ self.assertEqual(route.url,
+ '^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint))
+ else:
+ self.assertEqual(route.url,
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
+ # check method to function mapping
+ if endpoint.endswith('_post'):
+ method_map = 'post'
+ else:
+ method_map = 'get'
+ self.assertEqual(route.mapping[method_map], endpoint)