Add '.basename' and '.reverse_action()' to ViewSet (#5648)

* Router sets 'basename' on ViewSet

* Add 'ViewSet.reverse_action()' method

* Test router setting initkwargs
This commit is contained in:
Ryan P Kilby 2017-12-04 05:55:49 -05:00 committed by Carlton Gibson
parent c7df69ab77
commit 7855d3bd8b
5 changed files with 137 additions and 3 deletions

View File

@ -159,6 +159,21 @@ These decorators will route `GET` requests by default, but may also accept other
The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`
## 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.
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.
Using the example from the previous section:
```python
>>> view.reverse_action('set-password', args=['1'])
'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.
---
# API Reference

View File

@ -278,7 +278,12 @@ class SimpleRouter(BaseRouter):
if not prefix and regex[:2] == '^/':
regex = '^' + regex[2:]
view = viewset.as_view(mapping, **route.initkwargs)
initkwargs = route.initkwargs.copy()
initkwargs.update({
'basename': basename,
})
view = viewset.as_view(mapping, **initkwargs)
name = route.name.format(basename=basename)
ret.append(url(regex, view, name=name))

View File

@ -24,6 +24,7 @@ from django.utils.decorators import classonlymethod
from django.views.decorators.csrf import csrf_exempt
from rest_framework import generics, mixins, views
from rest_framework.reverse import reverse
class ViewSetMixin(object):
@ -46,10 +47,14 @@ class ViewSetMixin(object):
instantiated view, we need to totally reimplement `.as_view`,
and slightly modify the view function that is created and returned.
"""
# The suffix initkwarg is reserved for identifying the viewset type
# The suffix initkwarg is reserved for displaying the viewset type.
# eg. 'List' or 'Instance'.
cls.suffix = None
# Setting a basename allows a view to reverse its action urls. This
# value is provided by the router through the initkwargs.
cls.basename = None
# actions must not be empty
if not actions:
raise TypeError("The `actions` argument must be provided when "
@ -121,6 +126,15 @@ class ViewSetMixin(object):
self.action = self.action_map.get(method)
return request
def reverse_action(self, url_name, *args, **kwargs):
"""
Reverse the action for the given `url_name`.
"""
url_name = '%s-%s' % (self.basename, url_name)
kwargs.setdefault('request', self.request)
return reverse(url_name, *args, **kwargs)
class ViewSet(ViewSetMixin, views.APIView):
"""

View File

@ -7,6 +7,7 @@ from django.conf.urls import include, url
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.test import TestCase, override_settings
from django.urls import resolve
from rest_framework import permissions, serializers, viewsets
from rest_framework.compat import get_regex_pattern
@ -435,3 +436,18 @@ class TestRegexUrlPath(TestCase):
response = self.client.get('/regex/{}/detail/{}/'.format(pk, kwarg))
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8')) == {'pk': pk, 'kwarg': kwarg}
@override_settings(ROOT_URLCONF='tests.test_routers')
class TestViewInitkwargs(TestCase):
def test_suffix(self):
match = resolve('/example/notes/')
initkwargs = match.func.initkwargs
assert initkwargs['suffix'] == 'List'
def test_basename(self):
match = resolve('/example/notes/')
initkwargs = match.func.initkwargs
assert initkwargs['basename'] == 'routertestmodel'

View File

@ -1,7 +1,11 @@
from django.test import TestCase
from django.conf.urls import include, url
from django.db import models
from django.test import TestCase, override_settings
from rest_framework import status
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter
from rest_framework.test import APIRequestFactory
from rest_framework.viewsets import GenericViewSet
@ -22,6 +26,46 @@ class InstanceViewSet(GenericViewSet):
return Response({'view': self})
class Action(models.Model):
pass
class ActionViewSet(GenericViewSet):
queryset = Action.objects.all()
def list(self, request, *args, **kwargs):
pass
def retrieve(self, request, *args, **kwargs):
pass
@list_route()
def list_action(self, request, *args, **kwargs):
pass
@list_route(url_name='list-custom')
def custom_list_action(self, request, *args, **kwargs):
pass
@detail_route()
def detail_action(self, request, *args, **kwargs):
pass
@detail_route(url_name='detail-custom')
def custom_detail_action(self, request, *args, **kwargs):
pass
router = SimpleRouter()
router.register(r'actions', ActionViewSet)
router.register(r'actions-alt', ActionViewSet, base_name='actions-alt')
urlpatterns = [
url(r'^api/', include(router.urls)),
]
class InitializeViewSetsTestCase(TestCase):
def test_initialize_view_set_with_actions(self):
request = factory.get('/', '', content_type='application/json')
@ -65,3 +109,43 @@ class InitializeViewSetsTestCase(TestCase):
for attribute in ('args', 'kwargs', 'request', 'action_map'):
self.assertNotIn(attribute, dir(bare_view))
self.assertIn(attribute, dir(view))
@override_settings(ROOT_URLCONF='tests.test_viewsets')
class ReverseActionTests(TestCase):
def test_default_basename(self):
view = ActionViewSet()
view.basename = router.get_default_base_name(ActionViewSet)
view.request = None
assert view.reverse_action('list') == '/api/actions/'
assert view.reverse_action('list-action') == '/api/actions/list_action/'
assert view.reverse_action('list-custom') == '/api/actions/custom_list_action/'
assert view.reverse_action('detail', args=['1']) == '/api/actions/1/'
assert view.reverse_action('detail-action', args=['1']) == '/api/actions/1/detail_action/'
assert view.reverse_action('detail-custom', args=['1']) == '/api/actions/1/custom_detail_action/'
def test_custom_basename(self):
view = ActionViewSet()
view.basename = 'actions-alt'
view.request = None
assert view.reverse_action('list') == '/api/actions-alt/'
assert view.reverse_action('list-action') == '/api/actions-alt/list_action/'
assert view.reverse_action('list-custom') == '/api/actions-alt/custom_list_action/'
assert view.reverse_action('detail', args=['1']) == '/api/actions-alt/1/'
assert view.reverse_action('detail-action', args=['1']) == '/api/actions-alt/1/detail_action/'
assert view.reverse_action('detail-custom', args=['1']) == '/api/actions-alt/1/custom_detail_action/'
def test_request_passing(self):
view = ActionViewSet()
view.basename = router.get_default_base_name(ActionViewSet)
view.request = factory.get('/')
# Passing the view's request object should result in an absolute URL.
assert view.reverse_action('list') == 'http://testserver/api/actions/'
# Users should be able to explicitly not pass the view's request.
assert view.reverse_action('list', request=None) == '/api/actions/'