mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-02 03:20:12 +03:00
Adding @global_action and @global_link decorators
This commit is contained in:
parent
452eb81f5c
commit
e0c71f2bdf
|
@ -39,7 +39,7 @@ The example above would generate the following URL patterns:
|
|||
|
||||
### 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:
|
||||
|
||||
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):
|
||||
...
|
||||
|
||||
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'`
|
||||
|
||||
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
|
||||
|
||||
## 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>
|
||||
<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>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>PUT</td><td>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 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 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>PUT</td><td>update</td></tr>
|
||||
<tr><td>PATCH</td><td>partial_update</td></tr>
|
||||
|
|
|
@ -101,7 +101,7 @@ The default routers included with REST framework will provide routes for a stand
|
|||
def destroy(self, request, pk=None):
|
||||
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:
|
||||
|
||||
|
@ -131,13 +131,13 @@ For example:
|
|||
return Response(serializer.errors,
|
||||
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])
|
||||
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'])
|
||||
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 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/$`
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -127,3 +127,25 @@ def action(methods=['post'], **kwargs):
|
|||
func.kwargs = kwargs
|
||||
return func
|
||||
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
|
||||
|
|
|
@ -88,6 +88,16 @@ class SimpleRouter(BaseRouter):
|
|||
name='{basename}-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.
|
||||
Route(
|
||||
url=r'^{prefix}/{lookup}{trailing_slash}$',
|
||||
|
@ -141,10 +151,13 @@ class SimpleRouter(BaseRouter):
|
|||
|
||||
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 = []
|
||||
global_dynamic_routes = []
|
||||
for methodname in dir(viewset):
|
||||
attr = getattr(viewset, methodname)
|
||||
|
||||
httpmethods = getattr(attr, 'bind_to_methods', None)
|
||||
if httpmethods:
|
||||
if methodname in known_actions:
|
||||
|
@ -153,6 +166,14 @@ class SimpleRouter(BaseRouter):
|
|||
httpmethods = [method.lower() for method in httpmethods]
|
||||
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 = []
|
||||
for route in self.routes:
|
||||
if route.mapping == {'{httpmethod}': '{methodname}'}:
|
||||
|
@ -166,6 +187,17 @@ class SimpleRouter(BaseRouter):
|
|||
name=replace_methodname(route.name, methodname),
|
||||
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:
|
||||
# Standard route
|
||||
ret.append(route)
|
||||
|
|
|
@ -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 link, action, global_link, global_action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import SimpleRouter, DefaultRouter
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
@ -38,6 +38,30 @@ class BasicViewSet(viewsets.ViewSet):
|
|||
def link2(self, request, *args, **kwargs):
|
||||
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):
|
||||
def setUp(self):
|
||||
|
@ -62,6 +86,25 @@ class TestSimpleRouter(TestCase):
|
|||
for method in methods_map:
|
||||
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):
|
||||
uuid = models.CharField(max_length=20)
|
||||
|
|
Loading…
Reference in New Issue
Block a user