Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework dynamic list/detail actions #5705

Merged
merged 13 commits into from
Jan 25, 2018
2 changes: 1 addition & 1 deletion docs/api-guide/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ If you have specific requirements for creating schema endpoints that are accesse

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

@list_route(methods=['GET'])
@action(methods=['GET'], detail=False)
def schema(self, request):
meta = self.metadata_class()
data = meta.determine_metadata(request, self)
Expand Down
96 changes: 39 additions & 57 deletions docs/api-guide/routers.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,81 +81,62 @@ Router URL patterns can also be namespaces.

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.

### Extra link and actions
### Routing for extra actions

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:
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:

from myapp.permissions import IsAdminOrIsSelf
from rest_framework.decorators import detail_route
from rest_framework.decorators import action

class UserViewSet(ModelViewSet):
...

@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
@action(methods=['post'], detail=True, permission_classes=[IsAdminOrIsSelf])
Copy link
Collaborator

@carltongibson carltongibson Jan 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point the detail parameter is mysterious. (Worth explaining it below the example.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be sufficient to link to the viewset docs first? e.g., "go read this first, then come back here"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. (I was thinking along those lines... 🙂)

Perhaps moving the note from ln124 up and into a Note: section may be worthwhile. (Who reads about Routers before ViewSets?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the docs. Integrated line 124 into the start of the section.

def set_password(self, request, pk=None):
...

The following URL pattern would additionally be generated:
The following route would be generated:

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

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.
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.
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.

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

from myapp.permissions import IsAdminOrIsSelf
from rest_framework.decorators import detail_route
from rest_framework.decorators import action

class UserViewSet(ModelViewSet):
...

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

The above example would now generate the following URL pattern:

* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`

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.

For example, if you want to change the name of our custom action to `'user-change-password'`, you could write:

from myapp.permissions import IsAdminOrIsSelf
from rest_framework.decorators import detail_route

class UserViewSet(ModelViewSet):
...

@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_name='change-password')
def set_password(self, request, pk=None):
...

The above example would now generate the following URL pattern:

* URL pattern: `^users/{pk}/set_password/$` Name: `'user-change-password'`

You can also use url_path and url_name parameters together to obtain extra control on URL generation for custom views.

For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
* URL path: `^users/{pk}/change-password/$`
* URL name: `'user-change_password'`

# 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 `@detail_route` or `@list_route` 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 `@action` decorator.

<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>{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>{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>
<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>
<tr><td>DELETE</td><td>destroy</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>{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>
</table>

By default the URLs created by `SimpleRouter` are appended with a trailing slash.
Expand All @@ -180,12 +161,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 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>{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>{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>
<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>
<tr><td>DELETE</td><td>destroy</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>{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>
</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.
Expand All @@ -212,49 +193,50 @@ The arguments to the `Route` named tuple are:

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

**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.
**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.

## Customizing dynamic routes

You can also customize how the `@list_route` and `@detail_route` decorators are routed.
To route either or both of these decorators, include a `DynamicListRoute` and/or `DynamicDetailRoute` named tuple in the `.routes` list.
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:

The arguments to `DynamicListRoute` and `DynamicDetailRoute` are:
**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.

**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.
**name**: The name of the URL as used in `reverse` calls. May include the following format strings:

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

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

## Example

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

from rest_framework.routers import Route, DynamicDetailRoute, SimpleRouter
from rest_framework.routers import Route, DynamicRoute, SimpleRouter

class CustomReadOnlyRouter(SimpleRouter):
"""
A router for read-only APIs, which doesn't use trailing slashes.
"""
routes = [
Route(
url=r'^{prefix}$',
mapping={'get': 'list'},
name='{basename}-list',
initkwargs={'suffix': 'List'}
url=r'^{prefix}$',
mapping={'get': 'list'},
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
Route(
url=r'^{prefix}/{lookup}$',
url=r'^{prefix}/{lookup}$',
mapping={'get': 'retrieve'},
name='{basename}-detail',
initkwargs={'suffix': 'Detail'}
),
DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodnamehyphen}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
)
DynamicRoute(
url=r'^{prefix}/{lookup}/{url_path}$',
name='{basename}-{url_name}',
detail=True,
initkwargs={}
)
]

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

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

router = CustomReadOnlyRouter()
router.register('users', UserViewSet)
urlpatterns = router.urls
urlpatterns = router.urls

The following mappings would be generated...

Expand Down
46 changes: 29 additions & 17 deletions docs/api-guide/viewsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,16 @@ The default routers included with REST framework will provide routes for a stand
def destroy(self, request, pk=None):
pass

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

For example, you could restrict permissions to everything except the `list` action similar to this:
During dispatch, the following attributes are available on the `ViewSet`.

* `basename` - the base to use for the URL names that are created.
* `action` - the name of the current action (e.g., `list`, `create`).
* `detail` - boolean indicating if the current action is configured for a list or detail view.
* `suffix` - the display suffix for the viewset type - mirrors the `detail` attribute.

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:

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

## Marking extra actions for routing

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.
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.

For example:
A more complete example of extra actions:

from django.contrib.auth.models import User
from rest_framework import status
from rest_framework import viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer

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

@detail_route(methods=['post'])
@action(methods=['post'], detail=True)
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordSerializer(data=request.data)
Expand All @@ -151,7 +154,7 @@ For example:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)

@list_route()
@action(detail=False)
def recent_users(self, request):
recent_users = User.objects.all().order('-last_login')

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

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

@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
@action(methods=['post'], detail=True, permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...

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

@detail_route(methods=['post', 'delete'])
@action(methods=['post', 'delete'], detail=True)
def unset_password(self, request, pk=None):
...

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

To view all extra actions, call the `.get_extra_actions()` method.

## Reversing action URLs

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.
Expand All @@ -190,7 +195,14 @@ Using the example from the previous section:
'http://localhost:8000/api/users/1/set_password'
```

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.
Alternatively, you can use the `url_name` attribute set by the `@action` decorator.

```python
>>> view.reverse_action(view.set_password.url_name, args=['1'])
'http://localhost:8000/api/users/1/set_password'
```

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`.

---

Expand Down
29 changes: 29 additions & 0 deletions docs/topics/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,31 @@ You can determine your currently installed version using `pip freeze`:

---

## 3.8.x series

### 3.8.0

**Date**: [unreleased][3.8.0-milestone]

* Refactor dynamic route generation and improve viewset action introspectibility. [#5705][gh5705]

`ViewSet`s have been provided with new attributes and methods that allow
it to introspect its set of actions and the details of the current action.

* Merged `list_route` and `detail_route` into a single `action` decorator.
* Get all extra actions on a `ViewSet` with `.get_extra_actions()`.
* Extra actions now set the `url_name` and `url_path` on the decorated method.
* Enable action url reversing through `.reverse_action()` method (added in 3.7.4)
* Example reverse call: `self.reverse_action(self.custom_action.url_name)`
* Add `detail` initkwarg to indicate if the current action is operating on a
collection or a single instance.

Additional changes:

* Deprecated `list_route` & `detail_route` in favor of `action` decorator with `detail` boolean.
* Deprecated dynamic list/detail route variants in favor of `DynamicRoute` with `detail` boolean.
* Refactored the router's dynamic route generation.

## 3.7.x series

### 3.7.7
Expand Down Expand Up @@ -947,6 +972,7 @@ For older release notes, [please see the version 2.x documentation][old-release-
[3.7.5-milestone]: https://github.com/encode/django-rest-framework/milestone/63?closed=1
[3.7.6-milestone]: https://github.com/encode/django-rest-framework/milestone/64?closed=1
[3.7.7-milestone]: https://github.com/encode/django-rest-framework/milestone/65?closed=1
[3.8.0-milestone]: https://github.com/encode/django-rest-framework/milestone/61?closed=1

<!-- 3.0.1 -->
[gh2013]: https://github.com/encode/django-rest-framework/issues/2013
Expand Down Expand Up @@ -1760,3 +1786,6 @@ For older release notes, [please see the version 2.x documentation][old-release-
[gh5695]: https://github.com/encode/django-rest-framework/issues/5695
[gh5696]: https://github.com/encode/django-rest-framework/issues/5696
[gh5697]: https://github.com/encode/django-rest-framework/issues/5697

<!-- 3.8.0 -->
[gh5705]: https://github.com/encode/django-rest-framework/issues/5705
Loading