This commit is contained in:
Jesús Espino 2013-10-01 06:49:56 -07:00
commit fccdd05aab
6 changed files with 131 additions and 9 deletions

View File

@ -39,7 +39,7 @@ The example above would generate the following URL patterns:
### Extra link and actions ### Extra link and actions
Any methods on the viewset decorated with `@link` or `@action` will also be routed. Any methods on the viewset decorated with `@link`, `@global_link`, `@action` or `@global_action` will also be routed.
For example, given a method like this on the `UserViewSet` class: For example, given a method like this on the `UserViewSet` class:
from myapp.permissions import IsAdminOrIsSelf from myapp.permissions import IsAdminOrIsSelf
@ -49,20 +49,32 @@ For example, given a method like this on the `UserViewSet` class:
def set_password(self, request, pk=None): def set_password(self, request, pk=None):
... ...
The following URL pattern would additionally be generated: @global_action()
def login(self, request):
...
The following URL pattern would additionally be generated for `@link` and `@action`:
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'` * URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
The following URL pattern would additionally be generated for `@global_link` and `@global_action`:
* URL pattern: `^users/login/$` Name: `'user-global-login'`
**Note**: The `@global_action` and `@global_link` decorators generate urls with greater precedence than routes with `{pk}` params, take care to not hide valid instance urls with an action or link url (For example, on resources with a slug as pk).
# API Guide # API Guide
## SimpleRouter ## SimpleRouter
This router includes routes for the standard set of `list`, `create`, `retrieve`, `update`, `partial_update` and `destroy` actions. The viewset can also mark additional methods to be routed, using the `@link` or `@action` decorators. This router includes routes for the standard set of `list`, `create`, `retrieve`, `update`, `partial_update` and `destroy` actions. The viewset can also mark additional methods to be routed, using the `@link`, `@global_link`, `@action` or `@global_action` decorators.
<table border=1> <table border=1>
<tr><th>URL Style</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr> <tr><th>URL Style</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr>
<tr><td rowspan=2>{prefix}/</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr> <tr><td rowspan=2>{prefix}/</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
<tr><td>POST</td><td>create</td></tr> <tr><td>POST</td><td>create</td></tr>
<tr><td rowspan=2>{prefix}/{methodname}/[.format]</td><td>GET</td><td>@global_link decorated method</td><td rowspan=2>{basename}-global-{methodname}</td></tr>
<tr><td>POST</td><td>@global_action decorated method</td></tr>
<tr><td rowspan=4>{prefix}/{lookup}/</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr> <tr><td rowspan=4>{prefix}/{lookup}/</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
<tr><td>PUT</td><td>update</td></tr> <tr><td>PUT</td><td>update</td></tr>
<tr><td>PATCH</td><td>partial_update</td></tr> <tr><td>PATCH</td><td>partial_update</td></tr>
@ -87,6 +99,8 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
<tr><td>[.format]</td><td>GET</td><td>automatically generated root view</td><td>api-root</td></tr></tr> <tr><td>[.format]</td><td>GET</td><td>automatically generated root view</td><td>api-root</td></tr></tr>
<tr><td rowspan=2>{prefix}/[.format]</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr> <tr><td rowspan=2>{prefix}/[.format]</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
<tr><td>POST</td><td>create</td></tr> <tr><td>POST</td><td>create</td></tr>
<tr><td rowspan=2>{prefix}/{methodname}/[.format]</td><td>GET</td><td>@global_link decorated method</td><td rowspan=2>{basename}-global-{methodname}</td></tr>
<tr><td>POST</td><td>@global_action decorated method</td></tr>
<tr><td rowspan=4>{prefix}/{lookup}/[.format]</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr> <tr><td rowspan=4>{prefix}/{lookup}/[.format]</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
<tr><td>PUT</td><td>update</td></tr> <tr><td>PUT</td><td>update</td></tr>
<tr><td>PATCH</td><td>partial_update</td></tr> <tr><td>PATCH</td><td>partial_update</td></tr>

View File

@ -101,7 +101,7 @@ The default routers included with REST framework will provide routes for a stand
def destroy(self, request, pk=None): def destroy(self, request, pk=None):
pass pass
If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link` or `@action` decorators. The `@link` decorator will route `GET` requests, and the `@action` decorator will route `POST` requests. If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link`, `@global_link`, `@action` or `@global_action` decorators. The `@link` and `@global_link` decorators will route `GET` requests, and the `@action` and `@global_action` decorators will route `POST` requests.
For example: For example:
@ -131,13 +131,13 @@ For example:
return Response(serializer.errors, return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
The `@action` and `@link` decorators can additionally take extra arguments that will be set for the routed view only. For example... The `@action`, `@global_action`, `@link` and `@global_link` decorators can additionally take extra arguments that will be set for the routed view only. For example...
@action(permission_classes=[IsAdminOrIsSelf]) @action(permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None): def set_password(self, request, pk=None):
... ...
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example: The `@action` and `@global_action` decorators will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example:
@action(methods=['POST', 'DELETE']) @action(methods=['POST', 'DELETE'])
def unset_password(self, request, pk=None): def unset_password(self, request, pk=None):
@ -145,6 +145,17 @@ The `@action` decorator will route `POST` requests by default, but may also acce
The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$` The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`
The global actions allow to generate actions and links without the `{pk}` route part. For example:
@global_action()
def login(self, request):
...
@global_link()
def logout(self, request):
...
This action and link will then be available at the urls `^users/login/$` and `^users/logout/$`
--- ---

View File

@ -51,7 +51,7 @@ This time we've used the `ModelViewSet` class in order to get the complete set o
Notice that we've also used the `@link` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style. Notice that we've also used the `@link` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style.
Custom actions which use the `@link` decorator will respond to `GET` requests. We could have instead used the `@action` decorator if we wanted an action that responded to `POST` requests. Custom actions which use the `@link` or `@global_link` decorators will respond to `GET` requests. We could have instead used the `@action` or `@global_action` decorators if we wanted an action that responded to `POST` requests.
## Binding ViewSets to URLs explicitly ## Binding ViewSets to URLs explicitly

View File

@ -127,3 +127,25 @@ def action(methods=['post'], **kwargs):
func.kwargs = kwargs func.kwargs = kwargs
return func return func
return decorator return decorator
def global_link(**kwargs):
"""
Used to mark a method on a ViewSet that should be routed for GET requests.
"""
def decorator(func):
func.global_bind_to_methods = ['get']
func.global_kwargs = kwargs
return func
return decorator
def global_action(methods=['post'], **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for POST requests.
"""
def decorator(func):
func.global_bind_to_methods = methods
func.global_kwargs = kwargs
return func
return decorator

View File

@ -88,6 +88,16 @@ class SimpleRouter(BaseRouter):
name='{basename}-list', name='{basename}-list',
initkwargs={'suffix': 'List'} initkwargs={'suffix': 'List'}
), ),
# Dynamically generated routes.
# Generated using @global_action or @global_link decorators on methods of the viewset.
Route(
url=r'^{prefix}/{methodname}{trailing_slash}$',
mapping={
'{httpmethod}': '{globalmethodname}',
},
name='{basename}-global-{methodnamehyphen}',
initkwargs={}
),
# Detail route. # Detail route.
Route( Route(
url=r'^{prefix}/{lookup}{trailing_slash}$', url=r'^{prefix}/{lookup}{trailing_slash}$',
@ -141,10 +151,13 @@ class SimpleRouter(BaseRouter):
known_actions = flatten([route.mapping.values() for route in self.routes]) known_actions = flatten([route.mapping.values() for route in self.routes])
# Determine any `@action` or `@link` decorated methods on the viewset # Determine any `@action`, `@global_action`, `@link` or `@global_link`
# decorated methods on the viewset
dynamic_routes = [] dynamic_routes = []
global_dynamic_routes = []
for methodname in dir(viewset): for methodname in dir(viewset):
attr = getattr(viewset, methodname) attr = getattr(viewset, methodname)
httpmethods = getattr(attr, 'bind_to_methods', None) httpmethods = getattr(attr, 'bind_to_methods', None)
if httpmethods: if httpmethods:
if methodname in known_actions: if methodname in known_actions:
@ -153,6 +166,14 @@ class SimpleRouter(BaseRouter):
httpmethods = [method.lower() for method in httpmethods] httpmethods = [method.lower() for method in httpmethods]
dynamic_routes.append((httpmethods, methodname)) dynamic_routes.append((httpmethods, methodname))
httpmethods = getattr(attr, 'global_bind_to_methods', None)
if httpmethods:
if methodname in known_actions:
raise ImproperlyConfigured('Cannot use @global_action or @global_link decorator on '
'method "%s" as it is an existing route' % methodname)
httpmethods = [method.lower() for method in httpmethods]
global_dynamic_routes.append((httpmethods, methodname))
ret = [] ret = []
for route in self.routes: for route in self.routes:
if route.mapping == {'{httpmethod}': '{methodname}'}: if route.mapping == {'{httpmethod}': '{methodname}'}:
@ -166,6 +187,17 @@ class SimpleRouter(BaseRouter):
name=replace_methodname(route.name, methodname), name=replace_methodname(route.name, methodname),
initkwargs=initkwargs, initkwargs=initkwargs,
)) ))
elif route.mapping == {'{httpmethod}': '{globalmethodname}'}:
# Dynamic routes (@global_link or @global_action decorator)
for httpmethods, methodname in global_dynamic_routes:
initkwargs = route.initkwargs.copy()
initkwargs.update(getattr(viewset, methodname).global_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,
))
else: else:
# Standard route # Standard route
ret.append(route) ret.append(route)

View File

@ -4,7 +4,7 @@ from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from rest_framework import serializers, viewsets, permissions from rest_framework import serializers, viewsets, permissions
from rest_framework.compat import include, patterns, url from rest_framework.compat import include, patterns, url
from rest_framework.decorators import link, action from rest_framework.decorators import link, action, global_link, global_action
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
@ -38,6 +38,30 @@ class BasicViewSet(viewsets.ViewSet):
def link2(self, request, *args, **kwargs): def link2(self, request, *args, **kwargs):
return Response({'method': 'link2'}) return Response({'method': 'link2'})
class BasicWithGlobalsViewSet(viewsets.ViewSet):
def list(self, request, *args, **kwargs):
return Response({'method': 'list'})
@global_action()
def global_action1(self, request, *args, **kwargs):
return Response({'method': 'global_action1'})
@global_action()
def global_action2(self, request, *args, **kwargs):
return Response({'method': 'global_action2'})
@global_action(methods=['post', 'delete'])
def global_action3(self, request, *args, **kwargs):
return Response({'method': 'global_action3'})
@global_link()
def global_link1(self, request, *args, **kwargs):
return Response({'method': 'global_link1'})
@global_link()
def global_link2(self, request, *args, **kwargs):
return Response({'method': 'global_link2'})
class TestSimpleRouter(TestCase): class TestSimpleRouter(TestCase):
def setUp(self): def setUp(self):
@ -62,6 +86,25 @@ class TestSimpleRouter(TestCase):
for method in methods_map: for method in methods_map:
self.assertEqual(route.mapping[method], endpoint) self.assertEqual(route.mapping[method], endpoint)
def test_global_link_and_global_action_decorator(self):
routes = self.router.get_routes(BasicWithGlobalsViewSet)
decorator_routes = routes[1:]
# Make sure all these endpoints exist and none have been clobbered
for i, endpoint in enumerate(['global_action1', 'global_action2', 'global_action3', 'global_link1', 'global_link2']):
route = decorator_routes[i]
# check url listing
self.assertEqual(route.url,
'^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint))
# check method to function mapping
if endpoint == 'global_action3':
methods_map = ['post', 'delete']
elif endpoint.startswith('global_action'):
methods_map = ['post']
else:
methods_map = ['get']
for method in methods_map:
self.assertEqual(route.mapping[method], endpoint)
class RouterTestModel(models.Model): class RouterTestModel(models.Model):
uuid = models.CharField(max_length=20) uuid = models.CharField(max_length=20)