mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-03 05:04:31 +03:00
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:
parent
c7df69ab77
commit
7855d3bd8b
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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/'
|
||||
|
|
Loading…
Reference in New Issue
Block a user