Skip to content

Commit 7855d3b

Browse files
Ryan P Kilbycarltongibson
authored andcommitted
Add '.basename' and '.reverse_action()' to ViewSet (#5648)
* Router sets 'basename' on ViewSet * Add 'ViewSet.reverse_action()' method * Test router setting initkwargs
1 parent c7df69a commit 7855d3b

File tree

5 files changed

+137
-3
lines changed

5 files changed

+137
-3
lines changed

docs/api-guide/viewsets.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@ These decorators will route `GET` requests by default, but may also accept other
159159

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

162+
## Reversing action URLs
163+
164+
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.
165+
166+
Note that the `basename` is provided by the router during `ViewSet` registration. If you are not using a router, then you must provide the `basename` argument to the `.as_view()` method.
167+
168+
Using the example from the previous section:
169+
170+
```python
171+
>>> view.reverse_action('set-password', args=['1'])
172+
'http://localhost:8000/api/users/1/set_password'
173+
```
174+
175+
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.
176+
162177
---
163178

164179
# API Reference

rest_framework/routers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,12 @@ def get_urls(self):
278278
if not prefix and regex[:2] == '^/':
279279
regex = '^' + regex[2:]
280280

281-
view = viewset.as_view(mapping, **route.initkwargs)
281+
initkwargs = route.initkwargs.copy()
282+
initkwargs.update({
283+
'basename': basename,
284+
})
285+
286+
view = viewset.as_view(mapping, **initkwargs)
282287
name = route.name.format(basename=basename)
283288
ret.append(url(regex, view, name=name))
284289

rest_framework/viewsets.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from django.views.decorators.csrf import csrf_exempt
2525

2626
from rest_framework import generics, mixins, views
27+
from rest_framework.reverse import reverse
2728

2829

2930
class ViewSetMixin(object):
@@ -46,10 +47,14 @@ def as_view(cls, actions=None, **initkwargs):
4647
instantiated view, we need to totally reimplement `.as_view`,
4748
and slightly modify the view function that is created and returned.
4849
"""
49-
# The suffix initkwarg is reserved for identifying the viewset type
50+
# The suffix initkwarg is reserved for displaying the viewset type.
5051
# eg. 'List' or 'Instance'.
5152
cls.suffix = None
5253

54+
# Setting a basename allows a view to reverse its action urls. This
55+
# value is provided by the router through the initkwargs.
56+
cls.basename = None
57+
5358
# actions must not be empty
5459
if not actions:
5560
raise TypeError("The `actions` argument must be provided when "
@@ -121,6 +126,15 @@ def initialize_request(self, request, *args, **kwargs):
121126
self.action = self.action_map.get(method)
122127
return request
123128

129+
def reverse_action(self, url_name, *args, **kwargs):
130+
"""
131+
Reverse the action for the given `url_name`.
132+
"""
133+
url_name = '%s-%s' % (self.basename, url_name)
134+
kwargs.setdefault('request', self.request)
135+
136+
return reverse(url_name, *args, **kwargs)
137+
124138

125139
class ViewSet(ViewSetMixin, views.APIView):
126140
"""

tests/test_routers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.core.exceptions import ImproperlyConfigured
88
from django.db import models
99
from django.test import TestCase, override_settings
10+
from django.urls import resolve
1011

1112
from rest_framework import permissions, serializers, viewsets
1213
from rest_framework.compat import get_regex_pattern
@@ -435,3 +436,18 @@ def test_regex_url_path_detail(self):
435436
response = self.client.get('/regex/{}/detail/{}/'.format(pk, kwarg))
436437
assert response.status_code == 200
437438
assert json.loads(response.content.decode('utf-8')) == {'pk': pk, 'kwarg': kwarg}
439+
440+
441+
@override_settings(ROOT_URLCONF='tests.test_routers')
442+
class TestViewInitkwargs(TestCase):
443+
def test_suffix(self):
444+
match = resolve('/example/notes/')
445+
initkwargs = match.func.initkwargs
446+
447+
assert initkwargs['suffix'] == 'List'
448+
449+
def test_basename(self):
450+
match = resolve('/example/notes/')
451+
initkwargs = match.func.initkwargs
452+
453+
assert initkwargs['basename'] == 'routertestmodel'

tests/test_viewsets.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
from django.test import TestCase
1+
from django.conf.urls import include, url
2+
from django.db import models
3+
from django.test import TestCase, override_settings
24

35
from rest_framework import status
6+
from rest_framework.decorators import detail_route, list_route
47
from rest_framework.response import Response
8+
from rest_framework.routers import SimpleRouter
59
from rest_framework.test import APIRequestFactory
610
from rest_framework.viewsets import GenericViewSet
711

@@ -22,6 +26,46 @@ def dummy(self, request, *args, **kwargs):
2226
return Response({'view': self})
2327

2428

29+
class Action(models.Model):
30+
pass
31+
32+
33+
class ActionViewSet(GenericViewSet):
34+
queryset = Action.objects.all()
35+
36+
def list(self, request, *args, **kwargs):
37+
pass
38+
39+
def retrieve(self, request, *args, **kwargs):
40+
pass
41+
42+
@list_route()
43+
def list_action(self, request, *args, **kwargs):
44+
pass
45+
46+
@list_route(url_name='list-custom')
47+
def custom_list_action(self, request, *args, **kwargs):
48+
pass
49+
50+
@detail_route()
51+
def detail_action(self, request, *args, **kwargs):
52+
pass
53+
54+
@detail_route(url_name='detail-custom')
55+
def custom_detail_action(self, request, *args, **kwargs):
56+
pass
57+
58+
59+
router = SimpleRouter()
60+
router.register(r'actions', ActionViewSet)
61+
router.register(r'actions-alt', ActionViewSet, base_name='actions-alt')
62+
63+
64+
urlpatterns = [
65+
url(r'^api/', include(router.urls)),
66+
]
67+
68+
2569
class InitializeViewSetsTestCase(TestCase):
2670
def test_initialize_view_set_with_actions(self):
2771
request = factory.get('/', '', content_type='application/json')
@@ -65,3 +109,43 @@ def test_args_kwargs_request_action_map_on_self(self):
65109
for attribute in ('args', 'kwargs', 'request', 'action_map'):
66110
self.assertNotIn(attribute, dir(bare_view))
67111
self.assertIn(attribute, dir(view))
112+
113+
114+
@override_settings(ROOT_URLCONF='tests.test_viewsets')
115+
class ReverseActionTests(TestCase):
116+
def test_default_basename(self):
117+
view = ActionViewSet()
118+
view.basename = router.get_default_base_name(ActionViewSet)
119+
view.request = None
120+
121+
assert view.reverse_action('list') == '/api/actions/'
122+
assert view.reverse_action('list-action') == '/api/actions/list_action/'
123+
assert view.reverse_action('list-custom') == '/api/actions/custom_list_action/'
124+
125+
assert view.reverse_action('detail', args=['1']) == '/api/actions/1/'
126+
assert view.reverse_action('detail-action', args=['1']) == '/api/actions/1/detail_action/'
127+
assert view.reverse_action('detail-custom', args=['1']) == '/api/actions/1/custom_detail_action/'
128+
129+
def test_custom_basename(self):
130+
view = ActionViewSet()
131+
view.basename = 'actions-alt'
132+
view.request = None
133+
134+
assert view.reverse_action('list') == '/api/actions-alt/'
135+
assert view.reverse_action('list-action') == '/api/actions-alt/list_action/'
136+
assert view.reverse_action('list-custom') == '/api/actions-alt/custom_list_action/'
137+
138+
assert view.reverse_action('detail', args=['1']) == '/api/actions-alt/1/'
139+
assert view.reverse_action('detail-action', args=['1']) == '/api/actions-alt/1/detail_action/'
140+
assert view.reverse_action('detail-custom', args=['1']) == '/api/actions-alt/1/custom_detail_action/'
141+
142+
def test_request_passing(self):
143+
view = ActionViewSet()
144+
view.basename = router.get_default_base_name(ActionViewSet)
145+
view.request = factory.get('/')
146+
147+
# Passing the view's request object should result in an absolute URL.
148+
assert view.reverse_action('list') == 'http://testserver/api/actions/'
149+
150+
# Users should be able to explicitly not pass the view's request.
151+
assert view.reverse_action('list', request=None) == '/api/actions/'

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