Skip to content

Commit a9447da

Browse files
author
Ryan P Kilby
committed
Add 'extra actions' to ViewSet & browsable API
1 parent ba7e3fc commit a9447da

File tree

4 files changed

+53
-1
lines changed

4 files changed

+53
-1
lines changed

rest_framework/decorators.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,12 @@ def action(methods=None, detail=True, name=None, url_name=None, url_path=None, *
144144
def decorator(func):
145145
func.bind_to_methods = methods
146146
func.detail = detail
147+
func.name = name or pretty_name(func.__name__)
147148
func.url_name = url_name or func.__name__
148149
func.url_path = url_path or func.__name__
149150
func.kwargs = kwargs
150151
func.kwargs.update({
151-
'name': name or pretty_name(func.__name__),
152+
'name': func.name,
152153
'description': func.__doc__ or None
153154
})
154155

rest_framework/renderers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,11 @@ def get_description(self, view, status_code):
606606
def get_breadcrumbs(self, request):
607607
return get_breadcrumbs(request.path, request)
608608

609+
def get_extra_actions(self, view):
610+
if hasattr(view, 'get_extra_action_url_map'):
611+
return view.get_extra_action_url_map()
612+
return None
613+
609614
def get_filter_form(self, data, view, request):
610615
if not hasattr(view, 'get_queryset') or not hasattr(view, 'filter_backends'):
611616
return
@@ -692,6 +697,8 @@ def get_context(self, data, accepted_media_type, renderer_context):
692697
'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
693698
'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),
694699

700+
'extra_actions': self.get_extra_actions(view),
701+
695702
'filter_form': self.get_filter_form(data, view, request),
696703

697704
'raw_data_put_form': raw_data_put_form,

rest_framework/templates/rest_framework/base.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ <h4 class="text-center">Are you sure you want to delete this {{ name }}?</h4>
128128
</div>
129129
{% endif %}
130130

131+
{% if extra_actions %}
132+
<div class="dropdown" style="float: right; margin-right: 10px">
133+
<button class="btn btn-default" id="extra-actions-menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
134+
{% trans "Extra Actions" %}
135+
<span class="caret"></span>
136+
</button>
137+
<ul class="dropdown-menu" aria-labelledby="extra-actions-menu">
138+
{% for action_name, url in extra_actions|items %}
139+
<li><a href="{{ url }}">{{ action_name }}</a></li>
140+
{% endfor %}
141+
</ul>
142+
</div>
143+
{% endif %}
144+
131145
{% if filter_form %}
132146
<button style="float: right; margin-right: 10px" data-toggle="modal" data-target="#filtersModal" class="btn btn-default">
133147
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>

rest_framework/viewsets.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
"""
1919
from __future__ import unicode_literals
2020

21+
from collections import OrderedDict
2122
from functools import update_wrapper
2223
from inspect import getmembers
2324

25+
from django.urls import NoReverseMatch
2426
from django.utils.decorators import classonlymethod
2527
from django.views.decorators.csrf import csrf_exempt
2628

2729
from rest_framework import generics, mixins, views
30+
from rest_framework.reverse import reverse
2831

2932

3033
class ViewSetMixin(object):
@@ -134,6 +137,33 @@ def get_extra_actions(cls):
134137
"""
135138
return [method for _, method in getmembers(cls, _is_extra_action)]
136139

140+
def get_extra_action_url_map(self):
141+
"""
142+
Build a map of actions: urls for the extra actions. This requires the
143+
`detail` attribute to have been provided.
144+
"""
145+
action_map = OrderedDict()
146+
147+
# exit early if `detail` has not been provided
148+
if self.detail is None:
149+
return action_map
150+
151+
# filter for the relevant extra actions
152+
actions = [
153+
action for action in self.get_extra_actions()
154+
if action.detail == self.detail
155+
]
156+
157+
for action in actions:
158+
try:
159+
url_name = '%s-%s' % (self.basename, action.url_name)
160+
url = reverse(url_name, self.args, self.kwargs, request=self.request)
161+
action_map[action.name] = url
162+
except NoReverseMatch:
163+
pass # do nothing, URL requires additionalargs to reverse
164+
165+
return action_map
166+
137167

138168
def _is_extra_action(attr):
139169
return hasattr(attr, 'bind_to_methods')

0 commit comments

Comments
 (0)