Skip to content

Commit 3c9d16c

Browse files
Ryan P KilbyPierre Chiquet
authored andcommitted
Rework dynamic list/detail actions (encode#5705)
* Merge list/detail route decorators into 'action' * Merge dynamic routes, add 'detail' attribute * Add 'ViewSet.get_extra_actions()' * Refactor dynamic route checking & collection * Refactor dynamic route generation * Add 'ViewSet.detail' initkwarg * Fixup schema test * Add release notes for dynamic action changes * Replace list/detail route decorators in tests * Convert tabs to spaces in router docs * Update docs * Make 'detail' a required argument of 'action' * Improve router docs
1 parent f043052 commit 3c9d16c

File tree

12 files changed

+331
-204
lines changed

12 files changed

+331
-204
lines changed

docs/api-guide/metadata.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ If you have specific requirements for creating schema endpoints that are accesse
6767

6868
For example, the following additional route could be used on a viewset to provide a linkable schema endpoint.
6969

70-
@list_route(methods=['GET'])
70+
@action(methods=['GET'], detail=False)
7171
def schema(self, request):
7272
meta = self.metadata_class()
7373
data = meta.determine_metadata(request, self)

docs/api-guide/routers.md

Lines changed: 39 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -81,81 +81,62 @@ Router URL patterns can also be namespaces.
8181

8282
If using namespacing with hyperlinked serializers you'll also need to ensure that any `view_name` parameters on the serializers correctly reflect the namespace. In the example above you'd need to include a parameter such as `view_name='api:user-detail'` for serializer fields hyperlinked to the user detail view.
8383

84-
### Extra link and actions
84+
### Routing for extra actions
8585

86-
Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
87-
For example, given a method like this on the `UserViewSet` class:
86+
A viewset may [mark extra actions for routing][route-decorators] by decorating a method with the `@action` decorator. These extra actions will be included in the generated routes. For example, given the `set_password` method on the `UserViewSet` class:
8887

8988
from myapp.permissions import IsAdminOrIsSelf
90-
from rest_framework.decorators import detail_route
89+
from rest_framework.decorators import action
9190

9291
class UserViewSet(ModelViewSet):
9392
...
9493

95-
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
94+
@action(methods=['post'], detail=True, permission_classes=[IsAdminOrIsSelf])
9695
def set_password(self, request, pk=None):
9796
...
9897

99-
The following URL pattern would additionally be generated:
98+
The following route would be generated:
10099

101-
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
100+
* URL pattern: `^users/{pk}/set_password/$`
101+
* URL name: `'user-set-password'`
102102

103-
If you do not want to use the default URL generated for your custom action, you can instead use the url_path parameter to customize it.
103+
By default, the URL pattern is based on the method name, and the URL name is the combination of the `ViewSet.basename` and the hyphenated method name.
104+
If you don't want to use the defaults for either of these values, you can instead provide the `url_path` and `url_name` arguments to the `@action` decorator.
104105

105106
For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write:
106107

107108
from myapp.permissions import IsAdminOrIsSelf
108-
from rest_framework.decorators import detail_route
109+
from rest_framework.decorators import action
109110

110111
class UserViewSet(ModelViewSet):
111112
...
112113

113-
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_path='change-password')
114+
@action(methods=['post'], detail=True, permission_classes=[IsAdminOrIsSelf],
115+
url_path='change-password', url_name='change_password')
114116
def set_password(self, request, pk=None):
115117
...
116118

117119
The above example would now generate the following URL pattern:
118120

119-
* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
120-
121-
In the case you do not want to use the default name generated for your custom action, you can use the url_name parameter to customize it.
122-
123-
For example, if you want to change the name of our custom action to `'user-change-password'`, you could write:
124-
125-
from myapp.permissions import IsAdminOrIsSelf
126-
from rest_framework.decorators import detail_route
127-
128-
class UserViewSet(ModelViewSet):
129-
...
130-
131-
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_name='change-password')
132-
def set_password(self, request, pk=None):
133-
...
134-
135-
The above example would now generate the following URL pattern:
136-
137-
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-change-password'`
138-
139-
You can also use url_path and url_name parameters together to obtain extra control on URL generation for custom views.
140-
141-
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
121+
* URL path: `^users/{pk}/change-password/$`
122+
* URL name: `'user-change_password'`
142123

143124
# API Guide
144125

145126
## SimpleRouter
146127

147-
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.
128+
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 `@action` decorator.
148129

149130
<table border=1>
150131
<tr><th>URL Style</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr>
151132
<tr><td rowspan=2>{prefix}/</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
152133
<tr><td>POST</td><td>create</td></tr>
153-
<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>
134+
<tr><td>{prefix}/{url_path}/</td><td>GET, or as specified by `methods` argument</td><td>`@action(detail=False)` decorated method</td><td>{basename}-{url_name}</td></tr>
154135
<tr><td rowspan=4>{prefix}/{lookup}/</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
155136
<tr><td>PUT</td><td>update</td></tr>
156137
<tr><td>PATCH</td><td>partial_update</td></tr>
157138
<tr><td>DELETE</td><td>destroy</td></tr>
158-
<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>
139+
<tr><td>{prefix}/{lookup}/{url_path}/</td><td>GET, or as specified by `methods` argument</td><td>`@action(detail=True)` decorated method</td><td>{basename}-{url_name}</td></tr>
159140
</table>
160141

161142
By default the URLs created by `SimpleRouter` are appended with a trailing slash.
@@ -180,12 +161,12 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
180161
<tr><td>[.format]</td><td>GET</td><td>automatically generated root view</td><td>api-root</td></tr></tr>
181162
<tr><td rowspan=2>{prefix}/[.format]</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
182163
<tr><td>POST</td><td>create</td></tr>
183-
<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>
164+
<tr><td>{prefix}/{url_path}/[.format]</td><td>GET, or as specified by `methods` argument</td><td>`@action(detail=False)` decorated method</td><td>{basename}-{url_name}</td></tr>
184165
<tr><td rowspan=4>{prefix}/{lookup}/[.format]</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
185166
<tr><td>PUT</td><td>update</td></tr>
186167
<tr><td>PATCH</td><td>partial_update</td></tr>
187168
<tr><td>DELETE</td><td>destroy</td></tr>
188-
<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>
169+
<tr><td>{prefix}/{lookup}/{url_path}/[.format]</td><td>GET, or as specified by `methods` argument</td><td>`@action(detail=True)` decorated method</td><td>{basename}-{url_name}</td></tr>
189170
</table>
190171

191172
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.
@@ -212,49 +193,50 @@ The arguments to the `Route` named tuple are:
212193

213194
* `{basename}` - The base to use for the URL names that are created.
214195

215-
**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view. Note that the `suffix` argument is reserved for identifying the viewset type, used when generating the view name and breadcrumb links.
196+
**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view. Note that the `detail`, `basename`, and `suffix` arguments are reserved for viewset introspection and are also used by the browsable API to generate the view name and breadcrumb links.
216197

217198
## Customizing dynamic routes
218199

219-
You can also customize how the `@list_route` and `@detail_route` decorators are routed.
220-
To route either or both of these decorators, include a `DynamicListRoute` and/or `DynamicDetailRoute` named tuple in the `.routes` list.
200+
You can also customize how the `@action` decorator is routed. Include the `DynamicRoute` named tuple in the `.routes` list, setting the `detail` argument as appropriate for the list-based and detail-based routes. In addition to `detail`, the arguments to `DynamicRoute` are:
221201

222-
The arguments to `DynamicListRoute` and `DynamicDetailRoute` are:
202+
**url**: A string representing the URL to be routed. May include the same format strings as `Route`, and additionally accepts the `{url_path}` format string.
223203

224-
**url**: A string representing the URL to be routed. May include the same format strings as `Route`, and additionally accepts the `{methodname}` and `{methodnamehyphen}` format strings.
204+
**name**: The name of the URL as used in `reverse` calls. May include the following format strings:
225205

226-
**name**: The name of the URL as used in `reverse` calls. May include the following format strings: `{basename}`, `{methodname}` and `{methodnamehyphen}`.
206+
* `{basename}` - The base to use for the URL names that are created.
207+
* `{url_name}` - The `url_name` provided to the `@action`.
227208

228209
**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view.
229210

230211
## Example
231212

232213
The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention.
233214

234-
from rest_framework.routers import Route, DynamicDetailRoute, SimpleRouter
215+
from rest_framework.routers import Route, DynamicRoute, SimpleRouter
235216

236217
class CustomReadOnlyRouter(SimpleRouter):
237218
"""
238219
A router for read-only APIs, which doesn't use trailing slashes.
239220
"""
240221
routes = [
241222
Route(
242-
url=r'^{prefix}$',
243-
mapping={'get': 'list'},
244-
name='{basename}-list',
245-
initkwargs={'suffix': 'List'}
223+
url=r'^{prefix}$',
224+
mapping={'get': 'list'},
225+
name='{basename}-list',
226+
initkwargs={'suffix': 'List'}
246227
),
247228
Route(
248-
url=r'^{prefix}/{lookup}$',
229+
url=r'^{prefix}/{lookup}$',
249230
mapping={'get': 'retrieve'},
250231
name='{basename}-detail',
251232
initkwargs={'suffix': 'Detail'}
252233
),
253-
DynamicDetailRoute(
254-
url=r'^{prefix}/{lookup}/{methodnamehyphen}$',
255-
name='{basename}-{methodnamehyphen}',
256-
initkwargs={}
257-
)
234+
DynamicRoute(
235+
url=r'^{prefix}/{lookup}/{url_path}$',
236+
name='{basename}-{url_name}',
237+
detail=True,
238+
initkwargs={}
239+
)
258240
]
259241

260242
Let's take a look at the routes our `CustomReadOnlyRouter` would generate for a simple viewset.
@@ -269,7 +251,7 @@ Let's take a look at the routes our `CustomReadOnlyRouter` would generate for a
269251
serializer_class = UserSerializer
270252
lookup_field = 'username'
271253

272-
@detail_route()
254+
@action(detail=True)
273255
def group_names(self, request, pk=None):
274256
"""
275257
Returns a list of all the group names that the given
@@ -283,7 +265,7 @@ Let's take a look at the routes our `CustomReadOnlyRouter` would generate for a
283265

284266
router = CustomReadOnlyRouter()
285267
router.register('users', UserViewSet)
286-
urlpatterns = router.urls
268+
urlpatterns = router.urls
287269

288270
The following mappings would be generated...
289271

docs/api-guide/viewsets.md

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,16 @@ The default routers included with REST framework will provide routes for a stand
102102
def destroy(self, request, pk=None):
103103
pass
104104

105-
During dispatch the name of the current action is available via the `.action` attribute.
106-
You may inspect `.action` to adjust behaviour based on the current action.
105+
## Introspecting ViewSet actions
107106

108-
For example, you could restrict permissions to everything except the `list` action similar to this:
107+
During dispatch, the following attributes are available on the `ViewSet`.
108+
109+
* `basename` - the base to use for the URL names that are created.
110+
* `action` - the name of the current action (e.g., `list`, `create`).
111+
* `detail` - boolean indicating if the current action is configured for a list or detail view.
112+
* `suffix` - the display suffix for the viewset type - mirrors the `detail` attribute.
113+
114+
You may inspect these attributes to adjust behaviour based on the current action. For example, you could restrict permissions to everything except the `list` action similar to this:
109115

110116
def get_permissions(self):
111117
"""
@@ -119,16 +125,13 @@ For example, you could restrict permissions to everything except the `list` acti
119125

120126
## Marking extra actions for routing
121127

122-
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.
123-
124-
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.
128+
If you have ad-hoc methods that should be routable, you can mark them as such with the `@action` decorator. Like regular actions, extra actions may be intended for either a list of objects, or a single instance. To indicate this, set the `detail` argument to `True` or `False`. The router will configure its URL patterns accordingly. e.g., the `DefaultRouter` will configure detail actions to contain `pk` in their URL patterns.
125129

126-
For example:
130+
A more complete example of extra actions:
127131

128132
from django.contrib.auth.models import User
129-
from rest_framework import status
130-
from rest_framework import viewsets
131-
from rest_framework.decorators import detail_route, list_route
133+
from rest_framework import status, viewsets
134+
from rest_framework.decorators import action
132135
from rest_framework.response import Response
133136
from myapp.serializers import UserSerializer, PasswordSerializer
134137

@@ -139,7 +142,7 @@ For example:
139142
queryset = User.objects.all()
140143
serializer_class = UserSerializer
141144

142-
@detail_route(methods=['post'])
145+
@action(methods=['post'], detail=True)
143146
def set_password(self, request, pk=None):
144147
user = self.get_object()
145148
serializer = PasswordSerializer(data=request.data)
@@ -151,7 +154,7 @@ For example:
151154
return Response(serializer.errors,
152155
status=status.HTTP_400_BAD_REQUEST)
153156

154-
@list_route()
157+
@action(detail=False)
155158
def recent_users(self, request):
156159
recent_users = User.objects.all().order('-last_login')
157160

@@ -163,20 +166,22 @@ For example:
163166
serializer = self.get_serializer(recent_users, many=True)
164167
return Response(serializer.data)
165168

166-
The decorators can additionally take extra arguments that will be set for the routed view only. For example...
169+
The decorator can additionally take extra arguments that will be set for the routed view only. For example:
167170

168-
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
171+
@action(methods=['post'], detail=True, permission_classes=[IsAdminOrIsSelf])
169172
def set_password(self, request, pk=None):
170173
...
171174

172-
These decorators will route `GET` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example:
175+
These decorator will route `GET` requests by default, but may also accept other HTTP methods by setting the `methods` argument. For example:
173176

174-
@detail_route(methods=['post', 'delete'])
177+
@action(methods=['post', 'delete'], detail=True)
175178
def unset_password(self, request, pk=None):
176179
...
177180

178181
The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`
179182

183+
To view all extra actions, call the `.get_extra_actions()` method.
184+
180185
## Reversing action URLs
181186

182187
If you need to get the URL of an action, use the `.reverse_action()` method. This is a convenience wrapper for `reverse()`, automatically passing the view's `request` object and prepending the `url_name` with the `.basename` attribute.
@@ -190,7 +195,14 @@ Using the example from the previous section:
190195
'http://localhost:8000/api/users/1/set_password'
191196
```
192197

193-
The `url_name` argument should match the same argument to the `@list_route` and `@detail_route` decorators. Additionally, this can be used to reverse the default `list` and `detail` routes.
198+
Alternatively, you can use the `url_name` attribute set by the `@action` decorator.
199+
200+
```python
201+
>>> view.reverse_action(view.set_password.url_name, args=['1'])
202+
'http://localhost:8000/api/users/1/set_password'
203+
```
204+
205+
The `url_name` argument for `.reverse_action()` should match the same argument to the `@action` decorator. Additionally, this method can be used to reverse the default actions, such as `list` and `create`.
194206

195207
---
196208

docs/topics/release-notes.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,31 @@ You can determine your currently installed version using `pip freeze`:
3838

3939
---
4040

41+
## 3.8.x series
42+
43+
### 3.8.0
44+
45+
**Date**: [unreleased][3.8.0-milestone]
46+
47+
* Refactor dynamic route generation and improve viewset action introspectibility. [#5705][gh5705]
48+
49+
`ViewSet`s have been provided with new attributes and methods that allow
50+
it to introspect its set of actions and the details of the current action.
51+
52+
* Merged `list_route` and `detail_route` into a single `action` decorator.
53+
* Get all extra actions on a `ViewSet` with `.get_extra_actions()`.
54+
* Extra actions now set the `url_name` and `url_path` on the decorated method.
55+
* Enable action url reversing through `.reverse_action()` method (added in 3.7.4)
56+
* Example reverse call: `self.reverse_action(self.custom_action.url_name)`
57+
* Add `detail` initkwarg to indicate if the current action is operating on a
58+
collection or a single instance.
59+
60+
Additional changes:
61+
62+
* Deprecated `list_route` & `detail_route` in favor of `action` decorator with `detail` boolean.
63+
* Deprecated dynamic list/detail route variants in favor of `DynamicRoute` with `detail` boolean.
64+
* Refactored the router's dynamic route generation.
65+
4166
## 3.7.x series
4267

4368
### 3.7.7
@@ -940,6 +965,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
940965
[3.7.5-milestone]: https://github.com/encode/django-rest-framework/milestone/63?closed=1
941966
[3.7.6-milestone]: https://github.com/encode/django-rest-framework/milestone/64?closed=1
942967
[3.7.7-milestone]: https://github.com/encode/django-rest-framework/milestone/65?closed=1
968+
[3.8.0-milestone]: https://github.com/encode/django-rest-framework/milestone/61?closed=1
943969

944970
<!-- 3.0.1 -->
945971
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013
@@ -1750,3 +1776,6 @@ For older release notes, [please see the version 2.x documentation][old-release-
17501776
[gh5695]: https://github.com/encode/django-rest-framework/issues/5695
17511777
[gh5696]: https://github.com/encode/django-rest-framework/issues/5696
17521778
[gh5697]: https://github.com/encode/django-rest-framework/issues/5697
1779+
1780+
<!-- 3.8.0 -->
1781+
[gh5705]: https://github.com/encode/django-rest-framework/issues/5705

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy