Skip to content

Commit e36a1f3

Browse files
author
Ryan P Kilby
committed
Add 'name' and 'description' attributes to ViewSet
ViewSets may now provide their `name` and `description` attributes directly, instead of relying on view introspection to derive them. These attributes may also be provided with the view's initkwargs. The ViewSet `name` and `suffix` initkwargs are mutually exclusive. The `action` decorator now provides the `name` and `description` to the view's initkwargs. By default, these values are derived from the method name and its docstring. The `name` may be overridden by providing it as an argument to the decorator. The `get_view_name` and `get_view_description` hooks now provide the view instance to the handler, instead of the view class. The default implementations of these handlers now respect the `name`/`description`.
1 parent ee521ea commit e36a1f3

File tree

7 files changed

+99
-8
lines changed

7 files changed

+99
-8
lines changed

rest_framework/decorators.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import types
1212
import warnings
1313

14+
from django.forms.utils import pretty_name
1415
from django.utils import six
1516

1617
from rest_framework.views import APIView
@@ -130,7 +131,7 @@ def decorator(func):
130131
return decorator
131132

132133

133-
def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
134+
def action(methods=None, detail=None, name=None, url_path=None, url_name=None, **kwargs):
134135
"""
135136
Mark a ViewSet method as a routable action.
136137
@@ -147,9 +148,14 @@ def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs):
147148
def decorator(func):
148149
func.bind_to_methods = methods
149150
func.detail = detail
151+
func.name = name if name else pretty_name(func.__name__)
150152
func.url_path = url_path if url_path else func.__name__
151153
func.url_name = url_name if url_name else func.__name__.replace('_', '-')
152154
func.kwargs = kwargs
155+
func.kwargs.update({
156+
'name': func.name,
157+
'description': func.__doc__ or None
158+
})
153159
return func
154160
return decorator
155161

rest_framework/views.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,43 @@
2121
from rest_framework.utils import formatting
2222

2323

24-
def get_view_name(view_cls, suffix=None):
24+
def get_view_name(view):
2525
"""
2626
Given a view class, return a textual name to represent the view.
2727
This name is used in the browsable API, and in OPTIONS responses.
2828
2929
This function is the default for the `VIEW_NAME_FUNCTION` setting.
3030
"""
31-
name = view_cls.__name__
31+
# Name may be set by some Views, such as a ViewSet.
32+
name = getattr(view, 'name', None)
33+
if name is not None:
34+
return name
35+
36+
name = view.__class__.__name__
3237
name = formatting.remove_trailing_string(name, 'View')
3338
name = formatting.remove_trailing_string(name, 'ViewSet')
3439
name = formatting.camelcase_to_spaces(name)
40+
41+
# Suffix may be set by some Views, such as a ViewSet.
42+
suffix = getattr(view, 'suffix', None)
3543
if suffix:
3644
name += ' ' + suffix
3745

3846
return name
3947

4048

41-
def get_view_description(view_cls, html=False):
49+
def get_view_description(view, html=False):
4250
"""
4351
Given a view class, return a textual description to represent the view.
4452
This name is used in the browsable API, and in OPTIONS responses.
4553
4654
This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting.
4755
"""
48-
description = view_cls.__doc__ or ''
56+
# Description may be set by some Views, such as a ViewSet.
57+
description = getattr(view, 'description', None)
58+
if description is None:
59+
description = view.__class__.__doc__ or ''
60+
4961
description = formatting.dedent(smart_text(description))
5062
if html:
5163
return formatting.markup_description(description)
@@ -224,15 +236,15 @@ def get_view_name(self):
224236
browsable API.
225237
"""
226238
func = self.settings.VIEW_NAME_FUNCTION
227-
return func(self.__class__, getattr(self, 'suffix', None))
239+
return func(self)
228240

229241
def get_view_description(self, html=False):
230242
"""
231243
Return some descriptive text for the view, as used in OPTIONS responses
232244
and in the browsable API.
233245
"""
234246
func = self.settings.VIEW_DESCRIPTION_FUNCTION
235-
return func(self.__class__, html)
247+
return func(self, html)
236248

237249
# API policy instantiation methods
238250

rest_framework/viewsets.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@ def as_view(cls, actions=None, **initkwargs):
5252
instantiated view, we need to totally reimplement `.as_view`,
5353
and slightly modify the view function that is created and returned.
5454
"""
55+
# The name and description initkwargs may be explicitly overridden for
56+
# certain route confiugurations. eg, names of extra actions.
57+
cls.name = None
58+
cls.description = None
59+
5560
# The suffix initkwarg is reserved for displaying the viewset type.
61+
# This initkwarg should have no effect if the name is provided.
5662
# eg. 'List' or 'Instance'.
5763
cls.suffix = None
5864

@@ -79,6 +85,11 @@ def as_view(cls, actions=None, **initkwargs):
7985
raise TypeError("%s() received an invalid keyword %r" % (
8086
cls.__name__, key))
8187

88+
# name and suffix are mutually exclusive
89+
if 'name' in initkwargs and 'suffix' in initkwargs:
90+
raise TypeError("%s() received both `name` and `suffix`, which are "
91+
"mutually exclusive arguments." % (cls.__name__))
92+
8293
def view(request, *args, **kwargs):
8394
self = cls(**initkwargs)
8495
# We also store the mapping of request methods to actions,

tests/test_decorators.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,17 @@ class ActionDecoratorTestCase(TestCase):
175175
def test_defaults(self):
176176
@action(detail=True)
177177
def test_action(request):
178-
pass
178+
"""Description"""
179179

180180
assert test_action.bind_to_methods == ['get']
181181
assert test_action.detail is True
182+
assert test_action.name == 'Test action'
182183
assert test_action.url_path == 'test_action'
183184
assert test_action.url_name == 'test-action'
185+
assert test_action.kwargs == {
186+
'name': 'Test action',
187+
'description': 'Description',
188+
}
184189

185190
def test_detail_required(self):
186191
with pytest.raises(AssertionError) as excinfo:

tests/test_description.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,22 @@ class MockView(APIView):
8585
pass
8686
assert MockView().get_view_name() == 'Mock'
8787

88+
def test_view_name_uses_name_attribute(self):
89+
class MockView(APIView):
90+
name = 'Foo'
91+
assert MockView().get_view_name() == 'Foo'
92+
93+
def test_view_name_uses_suffix_attribute(self):
94+
class MockView(APIView):
95+
suffix = 'List'
96+
assert MockView().get_view_name() == 'Mock List'
97+
98+
def test_view_name_preferences_name_over_suffix(self):
99+
class MockView(APIView):
100+
name = 'Foo'
101+
suffix = 'List'
102+
assert MockView().get_view_name() == 'Foo'
103+
88104
def test_view_description_uses_docstring(self):
89105
"""Ensure view descriptions are based on the docstring."""
90106
class MockView(APIView):
@@ -112,6 +128,11 @@ class MockView(APIView):
112128

113129
assert MockView().get_view_description() == DESCRIPTION
114130

131+
def test_view_description_uses_description_attribute(self):
132+
class MockView(APIView):
133+
description = 'Foo'
134+
assert MockView().get_view_description() == 'Foo'
135+
115136
def test_view_description_can_be_empty(self):
116137
"""
117138
Ensure that if a view has no docstring,

tests/test_utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.conf.urls import url
55
from django.test import TestCase, override_settings
66

7+
from rest_framework.decorators import action
78
from rest_framework.routers import SimpleRouter
89
from rest_framework.serializers import ModelSerializer
910
from rest_framework.utils import json
@@ -43,6 +44,14 @@ class ResourceViewSet(ModelViewSet):
4344
serializer_class = ModelSerializer
4445
queryset = BasicModel.objects.all()
4546

47+
@action(detail=False)
48+
def list_action(self, request, *args, **kwargs):
49+
pass
50+
51+
@action(detail=True)
52+
def detail_action(self, request, *args, **kwargs):
53+
pass
54+
4655

4756
router = SimpleRouter()
4857
router.register(r'resources', ResourceViewSet)
@@ -119,6 +128,23 @@ def test_modelviewset_resource_instance_breadcrumbs(self):
119128
('Resource Instance', '/resources/1/')
120129
]
121130

131+
def test_modelviewset_list_action_breadcrumbs(self):
132+
url = '/resources/list_action/'
133+
assert get_breadcrumbs(url) == [
134+
('Root', '/'),
135+
('Resource List', '/resources/'),
136+
('List action', '/resources/list_action/'),
137+
]
138+
139+
def test_modelviewset_detail_action_breadcrumbs(self):
140+
url = '/resources/1/detail_action/'
141+
assert get_breadcrumbs(url) == [
142+
('Root', '/'),
143+
('Resource List', '/resources/'),
144+
('Resource Instance', '/resources/1/'),
145+
('Detail action', '/resources/1/detail_action/'),
146+
]
147+
122148

123149
class JsonFloatTests(TestCase):
124150
"""

tests/test_viewsets.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ def test_initialize_view_set_with_empty_actions(self):
9696
"when calling `.as_view()` on a ViewSet. "
9797
"For example `.as_view({'get': 'list'})`")
9898

99+
def test_initialize_view_set_with_both_name_and_suffix(self):
100+
with pytest.raises(TypeError) as excinfo:
101+
BasicViewSet.as_view(name='', suffix='', actions={
102+
'get': 'list',
103+
})
104+
105+
assert str(excinfo.value) == (
106+
"BasicViewSet() received both `name` and `suffix`, "
107+
"which are mutually exclusive arguments.")
108+
99109
def test_args_kwargs_request_action_map_on_self(self):
100110
"""
101111
Test a view only has args, kwargs, request, action_map

0 commit comments

Comments
 (0)