mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-02 11:30:12 +03:00
Merge e0c71f2bdf
into 452eb81f5c
This commit is contained in:
commit
fccdd05aab
|
@ -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>
|
||||||
|
|
|
@ -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/$`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user