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

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

Merged
merged 3 commits into from
Dec 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/api-guide/viewsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion rest_framework/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,12 @@ def get_urls(self):
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))

Expand Down
16 changes: 15 additions & 1 deletion rest_framework/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from django.views.decorators.csrf import csrf_exempt

from rest_framework import generics, mixins, views
from rest_framework.reverse import reverse


class ViewSetMixin(object):
Expand All @@ -46,10 +47,14 @@ def as_view(cls, actions=None, **initkwargs):
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 "
Expand Down Expand Up @@ -121,6 +126,15 @@ def initialize_request(self, request, *args, **kwargs):
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):
"""
Expand Down
16 changes: 16 additions & 0 deletions tests/test_routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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
Expand Down Expand Up @@ -435,3 +436,18 @@ def test_regex_url_path_detail(self):
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'
86 changes: 85 additions & 1 deletion tests/test_viewsets.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -22,6 +26,46 @@ def dummy(self, request, *args, **kwargs):
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')
Expand Down Expand Up @@ -65,3 +109,43 @@ def test_args_kwargs_request_action_map_on_self(self):
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/'