mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-11-01 00:17:40 +03:00 
			
		
		
		
	Merge branch 'static-routes' into 0.4.0
This commit is contained in:
		
						commit
						db25aaff3a
					
				|  | @ -35,12 +35,12 @@ The example above would generate the following URL patterns: | ||||||
| * URL pattern: `^accounts/$`  Name: `'account-list'` | * URL pattern: `^accounts/$`  Name: `'account-list'` | ||||||
| * URL pattern: `^accounts/{pk}/$`  Name: `'account-detail'` | * URL pattern: `^accounts/{pk}/$`  Name: `'account-detail'` | ||||||
| 
 | 
 | ||||||
| ### Extra link and actions | ### Registering additional routes | ||||||
| 
 | 
 | ||||||
| Any methods on the viewset decorated with `@link` or `@action` will also be routed. | Any methods on the viewset decorated with `@detail_route` or `@list_route` 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: | ||||||
| 
 | 
 | ||||||
|     @action(permission_classes=[IsAdminOrIsSelf]) |     @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) | ||||||
|     def set_password(self, request, pk=None): |     def set_password(self, request, pk=None): | ||||||
|         ... |         ... | ||||||
| 
 | 
 | ||||||
|  | @ -48,22 +48,24 @@ The following URL pattern would additionally be generated: | ||||||
| 
 | 
 | ||||||
| * URL pattern: `^users/{pk}/set_password/$`  Name: `'user-set-password'` | * URL pattern: `^users/{pk}/set_password/$`  Name: `'user-set-password'` | ||||||
| 
 | 
 | ||||||
|  | For more information see the viewset documentation on [marking extra actions for routing][route-decorators]. | ||||||
|  | 
 | ||||||
| # 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 `@detail_route` or `@list_route` 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>{prefix}/{methodname}/</td><td>GET, or as specified by `methods` argument</td><td>`@list_route` decorated method</td><td>{basename}-{methodname}</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> | ||||||
|     <tr><td>DELETE</td><td>destroy</td></tr> |     <tr><td>DELETE</td><td>destroy</td></tr> | ||||||
|     <tr><td rowspan=2>{prefix}/{lookup}/{methodname}/</td><td>GET</td><td>@link decorated method</td><td rowspan=2>{basename}-{methodname}</td></tr> |     <tr><td>{prefix}/{lookup}/{methodname}/</td><td>GET, or as specified by `methods` argument</td><td>`@detail_route` decorated method</td><td>{basename}-{methodname}</td></tr> | ||||||
|     <tr><td>POST</td><td>@action decorated method</td></tr> |  | ||||||
| </table> | </table> | ||||||
| 
 | 
 | ||||||
| By default the URLs created by `SimpleRouter` are appended with a trailing slash. | By default the URLs created by `SimpleRouter` are appended with a trailing slash. | ||||||
|  | @ -82,12 +84,12 @@ 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>{prefix}/{methodname}/[.format]</td><td>GET, or as specified by `methods` argument</td><td>`@list_route` decorated method</td><td>{basename}-{methodname}</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> | ||||||
|     <tr><td>DELETE</td><td>destroy</td></tr> |     <tr><td>DELETE</td><td>destroy</td></tr> | ||||||
|     <tr><td rowspan=2>{prefix}/{lookup}/{methodname}/[.format]</td><td>GET</td><td>@link decorated method</td><td rowspan=2>{basename}-{methodname}</td></tr> |     <tr><td>{prefix}/{lookup}/{methodname}/[.format]</td><td>GET, or as specified by `methods` argument</td><td>`@detail_route` decorated method</td><td>{basename}-{methodname}</td></tr> | ||||||
|     <tr><td>POST</td><td>@action decorated method</td></tr> |  | ||||||
| </table> | </table> | ||||||
| 
 | 
 | ||||||
| As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router. | As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router. | ||||||
|  | @ -144,3 +146,4 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an | ||||||
| You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. | You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. | ||||||
| 
 | 
 | ||||||
| [cite]: http://guides.rubyonrails.org/routing.html | [cite]: http://guides.rubyonrails.org/routing.html | ||||||
|  | [route-decorators]: viewsets.html#marking-extra-actions-for-routing | ||||||
|  | @ -61,7 +61,7 @@ There are two main advantages of using a `ViewSet` class over using a `View` cla | ||||||
| 
 | 
 | ||||||
| Both of these come with a trade-off.  Using regular views and URL confs is more explicit and gives you more control.  ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout. | Both of these come with a trade-off.  Using regular views and URL confs is more explicit and gives you more control.  ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout. | ||||||
| 
 | 
 | ||||||
| ## Marking extra methods for routing | ## Marking extra actions for routing | ||||||
| 
 | 
 | ||||||
| The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below: | The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below: | ||||||
| 
 | 
 | ||||||
|  | @ -92,14 +92,16 @@ 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 `@detail_route` or `@list_route` decorators. | ||||||
|  | 
 | ||||||
|  | The `@detail_route` decorator contains `pk` in its URL pattern and is intended for methods which require a single instance. The `@list_route` decorator is intended for methods which operate on a list of objects. | ||||||
| 
 | 
 | ||||||
| For example: | For example: | ||||||
| 
 | 
 | ||||||
|     from django.contrib.auth.models import User |     from django.contrib.auth.models import User | ||||||
|     from rest_framework import viewsets |  | ||||||
|     from rest_framework import status |     from rest_framework import status | ||||||
|     from rest_framework.decorators import action |     from rest_framework import viewsets | ||||||
|  |     from rest_framework.decorators import detail_route, list_route | ||||||
|     from rest_framework.response import Response |     from rest_framework.response import Response | ||||||
|     from myapp.serializers import UserSerializer, PasswordSerializer |     from myapp.serializers import UserSerializer, PasswordSerializer | ||||||
| 
 | 
 | ||||||
|  | @ -110,7 +112,7 @@ For example: | ||||||
|         queryset = User.objects.all() |         queryset = User.objects.all() | ||||||
|         serializer_class = UserSerializer |         serializer_class = UserSerializer | ||||||
| 
 | 
 | ||||||
|         @action() |         @detail_route(methods=['post']) | ||||||
|         def set_password(self, request, pk=None): |         def set_password(self, request, pk=None): | ||||||
|             user = self.get_object() |             user = self.get_object() | ||||||
|             serializer = PasswordSerializer(data=request.DATA) |             serializer = PasswordSerializer(data=request.DATA) | ||||||
|  | @ -122,15 +124,22 @@ 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... |         @list_route() | ||||||
|  |         def recent_users(self, request): | ||||||
|  |             recent_users = User.objects.all().order('-last_login') | ||||||
|  |             page = self.paginate_queryset(recent_users) | ||||||
|  |             serializer = self.get_pagination_serializer(page) | ||||||
|  |             return Response(serializer.data) | ||||||
| 
 | 
 | ||||||
|         @action(permission_classes=[IsAdminOrIsSelf]) | The decorators can additionally take extra arguments that will be set for the routed view only.  For example... | ||||||
|  | 
 | ||||||
|  |         @detail_route(methods=['post'], 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: | By default, the decorators will route `GET` requests, but may also accept other HTTP methods, by using the `methods` argument.  For example: | ||||||
| 
 | 
 | ||||||
|         @action(methods=['POST', 'DELETE']) |         @detail_route(methods=['post', 'delete']) | ||||||
|         def unset_password(self, request, pk=None): |         def unset_password(self, request, pk=None): | ||||||
|            ... |            ... | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ Here we've used `ReadOnlyModelViewSet` class to automatically provide the defaul | ||||||
| 
 | 
 | ||||||
| Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes.  We can remove the three views, and again replace them with a single class. | Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes.  We can remove the three views, and again replace them with a single class. | ||||||
| 
 | 
 | ||||||
|     from rest_framework.decorators import link |     from rest_framework.decorators import detail_route | ||||||
| 
 | 
 | ||||||
|     class SnippetViewSet(viewsets.ModelViewSet): |     class SnippetViewSet(viewsets.ModelViewSet): | ||||||
|         """ |         """ | ||||||
|  | @ -39,7 +39,7 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl | ||||||
|         permission_classes = (permissions.IsAuthenticatedOrReadOnly, |         permission_classes = (permissions.IsAuthenticatedOrReadOnly, | ||||||
|                               IsOwnerOrReadOnly,) |                               IsOwnerOrReadOnly,) | ||||||
| 
 | 
 | ||||||
|         @link(renderer_classes=[renderers.StaticHTMLRenderer]) |         @detail_route(renderer_classes=[renderers.StaticHTMLRenderer]) | ||||||
|         def highlight(self, request, *args, **kwargs): |         def highlight(self, request, *args, **kwargs): | ||||||
|             snippet = self.get_object() |             snippet = self.get_object() | ||||||
|             return Response(snippet.highlighted) |             return Response(snippet.highlighted) | ||||||
|  | @ -49,9 +49,9 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl | ||||||
| 
 | 
 | ||||||
| This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations. | This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations. | ||||||
| 
 | 
 | ||||||
| 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 `@detail_route` 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 `@detail_route` decorator will respond to `GET` requests.  We can use the `methods` argument if we wanted an action that responded to `POST` requests. | ||||||
| 
 | 
 | ||||||
| ## Binding ViewSets to URLs explicitly | ## Binding ViewSets to URLs explicitly | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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. | for writing function-based views with REST framework. | ||||||
| 
 | 
 | ||||||
| There are also various decorators for setting the API policies on function | 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. | used to annotate methods on viewsets that should be included by routers. | ||||||
| """ | """ | ||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals | ||||||
| from rest_framework.compat import six | from rest_framework.compat import six | ||||||
| from rest_framework.views import APIView | from rest_framework.views import APIView | ||||||
| import types | import types | ||||||
|  | import warnings | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def api_view(http_method_names): | def api_view(http_method_names): | ||||||
|  | @ -107,12 +108,40 @@ def permission_classes(permission_classes): | ||||||
|     return decorator |     return decorator | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def link(**kwargs): | def detail_route(methods=['get'], **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 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 | ||||||
|  | 
 | ||||||
|  | # These are now pending deprecation, in favor of `detail_route` and `list_route`. | ||||||
|  | 
 | ||||||
|  | def link(**kwargs): | ||||||
|  |     """ | ||||||
|  |     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): |     def decorator(func): | ||||||
|         func.bind_to_methods = ['get'] |         func.bind_to_methods = ['get'] | ||||||
|  |         func.detail = True | ||||||
|         func.kwargs = kwargs |         func.kwargs = kwargs | ||||||
|         return func |         return func | ||||||
|     return decorator |     return decorator | ||||||
|  | @ -120,10 +149,13 @@ def link(**kwargs): | ||||||
| 
 | 
 | ||||||
| def action(methods=['post'], **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): |     def decorator(func): | ||||||
|         func.bind_to_methods = methods |         func.bind_to_methods = methods | ||||||
|  |         func.detail = True | ||||||
|         func.kwargs = kwargs |         func.kwargs = kwargs | ||||||
|         return func |         return func | ||||||
|     return decorator |     return decorator | ||||||
|  | @ -26,6 +26,8 @@ from rest_framework.urlpatterns import format_suffix_patterns | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) | 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): | def replace_methodname(format_string, methodname): | ||||||
|  | @ -88,6 +90,14 @@ class SimpleRouter(BaseRouter): | ||||||
|             name='{basename}-list', |             name='{basename}-list', | ||||||
|             initkwargs={'suffix': '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. |         # Detail route. | ||||||
|         Route( |         Route( | ||||||
|             url=r'^{prefix}/{lookup}{trailing_slash}$', |             url=r'^{prefix}/{lookup}{trailing_slash}$', | ||||||
|  | @ -100,13 +110,10 @@ class SimpleRouter(BaseRouter): | ||||||
|             name='{basename}-detail', |             name='{basename}-detail', | ||||||
|             initkwargs={'suffix': 'Instance'} |             initkwargs={'suffix': 'Instance'} | ||||||
|         ), |         ), | ||||||
|         # Dynamically generated routes. |         # Dynamically generated detail routes. | ||||||
|         # Generated using @action or @link decorators on methods of the viewset. |         # Generated using @detail_route decorator on methods of the viewset. | ||||||
|         Route( |         DynamicDetailRoute( | ||||||
|             url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', |             url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', | ||||||
|             mapping={ |  | ||||||
|                 '{httpmethod}': '{methodname}', |  | ||||||
|             }, |  | ||||||
|             name='{basename}-{methodnamehyphen}', |             name='{basename}-{methodnamehyphen}', | ||||||
|             initkwargs={} |             initkwargs={} | ||||||
|         ), |         ), | ||||||
|  | @ -139,25 +146,42 @@ class SimpleRouter(BaseRouter): | ||||||
|         Returns a list of the Route namedtuple. |         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 |         # Determine any `@detail_route` or `@list_route` decorated methods on the viewset | ||||||
|         dynamic_routes = [] |         detail_routes = [] | ||||||
|  |         list_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) | ||||||
|  |             detail = getattr(attr, 'detail', True) | ||||||
|             if httpmethods: |             if httpmethods: | ||||||
|                 if methodname in known_actions: |                 if methodname in known_actions: | ||||||
|                     raise ImproperlyConfigured('Cannot use @action or @link decorator on ' |                     raise ImproperlyConfigured('Cannot use @detail_route or @list_route ' | ||||||
|                                                'method "%s" as it is an existing route' % methodname) |                                                'decorators on method "%s" ' | ||||||
|  |                                                'as it is an existing route' % methodname) | ||||||
|                 httpmethods = [method.lower() for method in httpmethods] |                 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 = [] |         ret = [] | ||||||
|         for route in self.routes: |         for route in self.routes: | ||||||
|             if route.mapping == {'{httpmethod}': '{methodname}'}: |             if isinstance(route, DynamicDetailRoute): | ||||||
|                 # Dynamic routes (@link or @action decorator) |                 # Dynamic detail routes (@detail_route decorator) | ||||||
|                 for httpmethods, methodname in dynamic_routes: |                 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 = route.initkwargs.copy() | ||||||
|                     initkwargs.update(getattr(viewset, methodname).kwargs) |                     initkwargs.update(getattr(viewset, methodname).kwargs) | ||||||
|                     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 detail_route, list_route | ||||||
| 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 | ||||||
|  | @ -18,23 +18,23 @@ class BasicViewSet(viewsets.ViewSet): | ||||||
|     def list(self, request, *args, **kwargs): |     def list(self, request, *args, **kwargs): | ||||||
|         return Response({'method': 'list'}) |         return Response({'method': 'list'}) | ||||||
| 
 | 
 | ||||||
|     @action() |     @detail_route(methods=['post']) | ||||||
|     def action1(self, request, *args, **kwargs): |     def action1(self, request, *args, **kwargs): | ||||||
|         return Response({'method': 'action1'}) |         return Response({'method': 'action1'}) | ||||||
| 
 | 
 | ||||||
|     @action() |     @detail_route(methods=['post']) | ||||||
|     def action2(self, request, *args, **kwargs): |     def action2(self, request, *args, **kwargs): | ||||||
|         return Response({'method': 'action2'}) |         return Response({'method': 'action2'}) | ||||||
| 
 | 
 | ||||||
|     @action(methods=['post', 'delete']) |     @detail_route(methods=['post', 'delete']) | ||||||
|     def action3(self, request, *args, **kwargs): |     def action3(self, request, *args, **kwargs): | ||||||
|         return Response({'method': 'action2'}) |         return Response({'method': 'action2'}) | ||||||
| 
 | 
 | ||||||
|     @link() |     @detail_route() | ||||||
|     def link1(self, request, *args, **kwargs): |     def link1(self, request, *args, **kwargs): | ||||||
|         return Response({'method': 'link1'}) |         return Response({'method': 'link1'}) | ||||||
| 
 | 
 | ||||||
|     @link() |     @detail_route() | ||||||
|     def link2(self, request, *args, **kwargs): |     def link2(self, request, *args, **kwargs): | ||||||
|         return Response({'method': 'link2'}) |         return Response({'method': 'link2'}) | ||||||
| 
 | 
 | ||||||
|  | @ -175,7 +175,7 @@ class TestActionKeywordArgs(TestCase): | ||||||
|         class TestViewSet(viewsets.ModelViewSet): |         class TestViewSet(viewsets.ModelViewSet): | ||||||
|             permission_classes = [] |             permission_classes = [] | ||||||
| 
 | 
 | ||||||
|             @action(permission_classes=[permissions.AllowAny]) |             @detail_route(methods=['post'], permission_classes=[permissions.AllowAny]) | ||||||
|             def custom(self, request, *args, **kwargs): |             def custom(self, request, *args, **kwargs): | ||||||
|                 return Response({ |                 return Response({ | ||||||
|                     'permission_classes': self.permission_classes |                     'permission_classes': self.permission_classes | ||||||
|  | @ -196,14 +196,14 @@ class TestActionKeywordArgs(TestCase): | ||||||
| 
 | 
 | ||||||
| class TestActionAppliedToExistingRoute(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 |     to an existing route | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     def test_exception_raised_when_action_applied_to_existing_route(self): |     def test_exception_raised_when_action_applied_to_existing_route(self): | ||||||
|         class TestViewSet(viewsets.ModelViewSet): |         class TestViewSet(viewsets.ModelViewSet): | ||||||
| 
 | 
 | ||||||
|             @action() |             @detail_route(methods=['post']) | ||||||
|             def retrieve(self, request, *args, **kwargs): |             def retrieve(self, request, *args, **kwargs): | ||||||
|                 return Response({ |                 return Response({ | ||||||
|                     'hello': 'world' |                     'hello': 'world' | ||||||
|  | @ -214,3 +214,49 @@ class TestActionAppliedToExistingRoute(TestCase): | ||||||
| 
 | 
 | ||||||
|         with self.assertRaises(ImproperlyConfigured): |         with self.assertRaises(ImproperlyConfigured): | ||||||
|             self.router.urls |             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) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user